尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

.NET 10 Web API从零搭建:Controllers、EF Core、SQL Server与DTO实战指南

.NET 10 Web API从零搭建:Controllers、EF Core、SQL Server与DTO实战指南 最近有朋友问我用 .NET 10 从零搭一个 Web API到底该先装什么、先建什么我第一反应不是回答命令而是先反问他一句你是在搭一个 demo还是在做一个以后要长期维护的接口服务如果只是 demodotnet new webapi跑起来就够了但如果是真实项目控制器Controllers、EF Core、SQL Server、DTOs 这四个概念看起来各管一段真正把它们串成一个边界清晰、后续好扩展的架构才是难点。很多人卡住的地方不是某个技术不会用而是没有理解这一套东西到底在协作中扮演什么角色。这篇博客我不会只给你命令行清单而是按“从零搭建一个可维护的 .NET 10 Web API”这条线把设计思路、关键代码、排查路径和适用边界一次讲清楚。你跟着走完得到的不仅是一个能跑的 CRUD 项目更是一个可以继续往里面加业务逻辑的骨架。1. 不要急着生成项目先想清楚四个关键词各自承担什么角色先花五分钟想清楚职责比多装一个包、多写一个类更有价值。.NET 10 Web API 这个壳本身只解决“接收 HTTP 请求并返回 HTTP 响应”的问题。真正让业务跑起来的是 Controller、EF Core、SQL Server 和 DTO 这四样东西之间的协作。1.1 一个最小但完整的请求链路假设你要做一个产品管理接口用户发一个GET /api/products背后大概发生过这样的链路ASP.NET Core 路由系统把请求交给 ProductsController 对应的 Action。Controller 调用 EF Core 提供的 DbContext 查询 SQL Server。查询结果从实体对象转换为 DTO 对象。Controller 把 DTO 序列化成 JSON 返回给调用方。这条链路的顺序看起来很简单但很多人写代码时容易把步骤搞混。比如直接在 Controller 里操作数据库或者直接把实体序列化返回或者把 DTO 当成数据库模型一路往下传。短项目没问题一旦业务变复杂会出现大量重复代码和难以追溯的数据流。先把链路在脑子里固定住Controller 是入口DbContext 是数据访问入口SQL Server 是存储DTO 是接口层的数据契约。它们各管一段谁也不要越界。1.2 先定实体和 DTO再让控制器去组装很多初学者习惯先建 Controller再顺便补实体和 DTO。我的建议是反过来先把实体和 DTO 定义出来再写 Controller。实体是数据库表的映射它代表数据的存储形状。比如 Product 实体字段可能包括 Id、Name、Price、CreatedAt。DTO 是接口对外暴露的数据形状它不必和实体一模一样。你可以把实体里不想暴露的字段去掉也可以把多个实体的字段组合成一个 DTO。这样设计的原因很简单数据库结构、业务逻辑、接口契约三者是不同频率的变化。数据库加一个内部字段不等于接口要把这个字段返回给客户端接口新增一个字段也不一定需要建数据库列。先定义 DTO会让接口边界更清晰。所以第零步不是敲命令而是回答几个问题要管理哪些核心业务对象每个对象在数据库里有哪些字段对外接口需要暴露哪些字段客户端提交数据时允许提交哪些字段想明白这几个问题后面的代码才有方向。2. 环境准备与项目骨架把“新版本”当成一个普通项目来对待.NET 10 目前仍在快速演进中不同预览版或正式版之间的命令差异不大但依赖包版本和部分 API 可能会有调整。建议不要背版本号而是学会看本机环境。2.1 需要准备的三个基础环境搭建这个项目至少需要准备三样东西.NET 10 SDKSQL ServerEF Core 命令行工具.NET 10 SDK在终端里执行dotnet --version如果返回的不是 10.x说明本机当前使用的 SDK 不是 .NET 10或者你需要在项目文件里指定目标框架。创建项目时可以用-f net10.0来指定框架版本前提是本机已经安装了对应 SDK。SQL Server选择哪种 SQL Server 取决于你的使用场景本地学习可以用 SQL Server 2022 Developer Edition 或 Express。容器化开发用 Docker 跑一个 SQL Server Linux 容器更方便可随时重置环境。生产环境要与运维同学确认版本、实例名称、账号权限和备份策略。无论用哪个版本需要确认三件事服务是否启动、连接字符串是否可访问、账号是否有建库建表权限。EF Core 命令行工具EF Core 需要单独的dotnet-ef工具来执行迁移命令。常见安装方式dotnet tool install --global dotnet-ef安装后可以用dotnet ef --version验证。如果之前安装过可以执行dotnet tool update --global dotnet-ef更新到与 SDK 匹配的版本。2.2 创建一个分层的 Web API 项目使用命令行创建项目dotnet new webapi -n MyShop.Api -f net10.0这个命令会生成一个带天气示例的 Web API 项目。接下来我会建议你按职责建立目录而不是把所有类都放在根目录下。常见的最小分层结构MyShop.Api/ ├── Controllers/ # 接口入口 ├── Data/ # DbContext 和 EF Core 配置 ├── Dtos/ # 请求/响应模型 ├── Entities/ # 数据库实体 ├── appsettings.json └── Program.cs目录名字可以按团队习惯调整但分层逻辑要保持。这样做的核心原因是让“数据访问”和“接口表达”在物理上分属不同位置避免 new 项目三个月后代码变成一堆没有归属的类。创建完目录结构后执行一次dotnet build确保项目能编译通过再继续装包。2.3 验证默认项目能不能跑是排错的第一步接下来的操作只做一件事让默认项目启动一次访问一下示例接口确认 HTTP 链路本身是通的。dotnet run浏览器打开http://localhost:5xxx/swagger如果能看到 Swagger UI说明 ASP.NET Core 这一层没问题。这一步很重要因为后面你加入 EF Core、SQL Server、自定义 DTO 后报错来源会变多。如果连默认项目都跑不起来说明问题在环境或模板而不是你的业务代码。从工程经验看默认项目跑不通通常有以下原因端口被占用。SDK 版本和项目目标框架不匹配。缺少 NuGet 包或本地 NuGet 源异常。防火墙拦截了 localhost 请求。先排除这些原因再继续写代码会省去很多困惑。3. 用 EF Core 把实体和 SQL Server 连起来连接字符串、迁移与常见坑EF Core 是 .NET 生态里的对象关系映射框架它让你用 C# 类来操作数据库而不是手写 SQL。但这个“不用写 SQL”是有条件的你要先把实体、上下文、连接字符串和迁移机制配置好。3.1 引入 EF Core 相关包在项目目录下执行dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Design第一行是 SQL Server 提供程序第二行是设计时工具支持。缺少 Design 包时dotnet ef命令会报找不到相关服务。如果你要用迁移功能还需要确认dotnet-ef工具已经安装。这一步容易出错的是版本不匹配。建议所有 EF Core 相关包使用同一个大版本避免运行时程序集冲突。具体版本号以 NuGet 当前稳定版为准不要直接照抄旧项目的数字。3.2 定义 DbContext 和连接字符串假设你有一个 Product 实体对应的 DbContext 可以这样定义using Microsoft.EntityFrameworkCore; namespace MyShop.Api.Data; public class AppDbContext : DbContext { public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } public DbSetProduct Products SetProduct(); }这里的DbSetProduct对应数据库里的 Products 表。使用SetProduct()是较新的写法能避免非空警告。连接字符串写在appsettings.json里{ ConnectionStrings: { DefaultConnection: Serverlocalhost;DatabaseMyShopDb;User Idsa;PasswordYourPassword;TrustServerCertificateTrue; }, Logging: { LogLevel: { Default: Information } } }连接字符串是新手最容易出问题的地方。常见错误包括用了本机实例名但 SQL Server 服务没启动。用了 Windows 认证但程序运行账户没有权限。没有配置TrustServerCertificateTrue导致 SQL Server 证书错误。密码包含特殊字符未做转义。在Program.cs注册 DbContext 的常见写法如下builder.Services.AddDbContextAppDbContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(DefaultConnection)));这段代码的意思是当 Controller 需要AppDbContext实例时ASP.NET Core 依赖注入容器会帮你创建并且统一使用配置好的连接字符串。3.3 迁移就是给数据库做版本管理迁移是 EF Core 把实体模型转换成数据库 schema 变更的一种机制。它不是“生成一个数据库”而是“记录每次表结构变化”。常用命令dotnet ef migrations add InitialCreate dotnet ef database update第一条命令会在项目里生成 Migrations 目录里面包含本次模型快照和变更操作。第二条命令会把变更应用到数据库。迁移真正重要的地方是团队协作。生产环境不能随便删库重建而迁移文件可以提交到版本库其他人执行dotnet ef database update就能把本地库升级到最新结构。如果数据库已经是旧的新增迁移文件后再执行更新EF Core 会对比历史迁移只执行新增部分。所以迁移文件最好不要删除也不要手动改数据库表结构否则两边会不一致。3.4 这个环节最常见的三个坑坑一迁移命令报找不到项目如果解决方案里有多个项目dotnet ef需要知道哪个是启动项目哪个包含 DbContext。常见写法dotnet ef migrations add InitialCreate --project MyShop.Api --startup-project MyShop.Api这里的--project指向包含 DbContext 的项目--startup-project指向启动项目。坑二数据库更新时提示 pending changes通常是你改了实体类但没有生成新的迁移文件。解决方法是先执行dotnet ef migrations add生成迁移再执行dotnet ef database update而不是直接改数据库。坑三运行时找不到连接字符串如果你把连接字符串放在appsettings.Development.json但项目以 Production 环境启动就会读不到。建议先统一在appsettings.json里保留一份可用的开发配置再根据环境覆盖。4. Controllers 和 DTOs接口层不是简单地把实体扔出去Controller 是 HTTP 请求的入口但它不应该承担数据库查询和业务规则的全部工作。它更像一个协调者把请求输入变成领域操作再把结果整理成响应。4.1 为什么接口层推荐使用 DTO一个常见错误是直接把实体返回给客户端。短项目看起来省事但长期会有几个问题实体里包含数据库内部字段比如CreatedAt、UpdatedAt、TenantId这些可能不应该暴露给前端。实体字段和接口字段不总是一一对应直接返回实体等于把数据库结构泄露给调用方。EF Core 的实体可能包含导航属性序列化时容易产生循环引用导致接口直接报错。DTO 的作用是定义接口层的数据契约。它可以是一个独立的 record 或 class只包含你需要返回或接收的字段。这样即使数据库表改变了内部结构只要 DTO 不变调用方就不受影响。4.2 一个基础 CRUD 控制器的写法假设有一个 DTO 定义如下namespace MyShop.Api.Dtos; public record ProductDto(int Id, string Name, decimal Price); public record CreateProductRequest(string Name, decimal Price); public record UpdateProductRequest(string Name, decimal Price);对应的 Controller 可以这样写using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using MyShop.Api.Data; using MyShop.Api.Dtos; using MyShop.Api.Entities; namespace MyShop.Api.Controllers; [ApiController] [Route(api/[controller])] public class ProductsController : ControllerBase { private readonly AppDbContext _context; public ProductsController(AppDbContext context) { _context context; } [HttpGet] public async TaskActionResultIEnumerableProductDto GetProducts() { var products await _context.Products .Select(p new ProductDto(p.Id, p.Name, p.Price)) .ToListAsync(); return Ok(products); } [HttpGet({id})] public async TaskActionResultProductDto GetProduct(int id) { var product await _context.Products.FindAsync(id); if (product is null) { return NotFound(); } return Ok(new ProductDto(product.Id, product.Name, product.Price)); } [HttpPost] public async TaskActionResultProductDto CreateProduct(CreateProductRequest request) { var product new Product { Name request.Name, Price request.Price }; _context.Products.Add(product); await _context.SaveChangesAsync(); return CreatedAtAction(nameof(GetProduct), new { id product.Id }, new ProductDto(product.Id, product.Name, product.Price)); } [HttpPut({id})] public async TaskIActionResult UpdateProduct(int id, UpdateProductRequest request) { var product await _context.Products.FindAsync(id); if (product is null) { return NotFound(); } product.Name request.Name; product.Price request.Price; await _context.SaveChangesAsync(); return NoContent(); } [HttpDelete({id})] public async TaskIActionResult DeleteProduct(int id) { var product await _context.Products.FindAsync(id); if (product is null) { return NotFound(); } _context.Products.Remove(product); await _context.SaveChangesAsync(); return NoContent(); } }这段代码的重点不是复制而是理解几个设计选择ActionResultT让接口既能返回Ok和NotFound也能返回强类型 DTO。Select把实体直接映射成 DTO避免把多余字段带回内存。CreatedAtAction是 REST 风格中创建资源后的常见返回方式它告诉客户端新资源的访问位置。UpdateProduct返回NoContent表示更新成功但没有响应体这是常见约定。4.3 从简单映射到项目变大之后的映射管理上面的写法适合商品这一层但真实项目的实体和 DTO 往往更复杂。比如一个订单既有用户信息又有订单项还涉及多个表。再在 Controller 里手动写Select表达式会非常冗长。这时候可以引入 AutoMapper 等映射工具也可以自己写映射扩展方法。我的建议是不要一上来就引入映射框架而是先用最直接的方式让 DTO 和实体字段少一些时保持简单等字段开始变多再抽一层MappingProfile或IProductMapper。核心目的是让 Controller 不关心“实体转 DTO”的具体实现只关心“输入输出形状”。5. 从“能跑”到“能上线”验证、异常处理、日志和配置隔离一个能跑的 CRUD 接口离“能上线”还有距离。你还需要处理非法输入、未知异常、日志记录和不同环境下的配置差异。这些不是“锦上添花”而是真实项目的基本盘。5.1 用模型验证拦住不合法输入在 Controller 参数上直接使用 DTO 时ASP.NET Core 会根据[ApiController]特性自动执行模型验证。DTO 字段可以加特性using System.ComponentModel.DataAnnotations; public record CreateProductRequest { [Required] [MaxLength(50)] public string Name { get; set; } string.Empty; [Range(0.01, double.MaxValue)] public decimal Price { get; set; } }这样当客户端传了空 Name 或负价格时接口会返回 400 Bad Request而不是进入业务代码。你可以在 Controller 里手动写验证逻辑但使用数据注解是更轻量的方式。要注意的是DTO 里的数据注解主要用于“输入验证”不是“数据库约束”。数据库字段长度和唯一性约束仍要单独处理比如 EF Core 的HasMaxLength或HasIndex配置。5.2 全局异常处理和日志默认情况下未处理异常会直接造成 500并在开发环境暴露堆栈信息。生产环境需要一个统一的异常处理策略避免把内部信息返回给客户端。一种方式是使用内置的异常处理中间件另一种是自定义ExceptionFilter。最简单的做法是在Program.cs里注册一个全局异常处理app.UseExceptionHandler(exceptionHandlerApp { exceptionHandlerApp.Run(async context { context.Response.StatusCode StatusCodes.Status500InternalServerError; await context.Response.WriteAsJsonAsync(new { error 服务器内部错误 }); }); });这只是一个最小方案。更精细的做法是把异常类型映射到不同的状态码比如“资源不存在”返回 404“参数不合法”返回 400“权限不足”返回 403。日志同样重要。至少在异常处理里记录ILogger这样问题出现时能看到堆栈和上下文var logger context.RequestServices.GetRequiredServiceILoggerProgram(); logger.LogError(ex, 请求处理发生未处理异常);5.3 环境配置与密钥保护连接字符串、数据库密码、第三方 API Key 都不应该硬编码在代码里。ASP.NET Core 的环境配置机制已经处理了这个问题。开发环境可以使用 User Secretsdotnet user-secrets init dotnet user-secrets set ConnectionStrings:DefaultConnection Serverlocalhost;DatabaseMyShopDb;...部署到生产环境时通过环境变量或部署平台的配置中心注入连接字符串。这样appsettings.json里的敏感信息可以留空或保留开发环境占位值。5.4 这些工程化步骤不能省只看 demo你可能会觉得这些步骤多此一举。但放到真实业务里任何一步缺失都会造成线上事故或排障困难。模型验证缺失垃圾数据会进库异常处理缺失客户端会看到一屏堆栈日志缺失出了故障根本无从下手配置写死换环境就要重新改代码部署。如果一个项目只用来学习前面四节的内容已经足够。如果要发布到测试环境或生产环境这一节的项目必须补上。6. 七步排查链路当接口报错时按这个顺序找问题技术文章如果只讲正确路径不讲排查方法读者遇到问题还是会卡住。这里我总结一套七步排查链路适用于这个项目里大多数报错场景。6.1 先看现象再动代码不要一看到报错就乱改代码。先把现象记录清楚是接口请求直接超时是返回 500 错误是返回空数组但数据库有数据是插入数据时报数据库异常是编译期错误还是运行期错误现象不同排查方向完全不同。6.2 按输入、环境、依赖、参数、工具边界逐层排查第一步看输入如果是新增或更新接口先确认客户端传参格式是否与服务端 DTO 一致。IDE 里直接对接口发请求时可以先用最小字段测试。特别检查 JSON 字段名的大小写、日期格式、可空字段。第二步看环境数据库连不上、Swagger 打不开、用户密钥读不到都属于环境问题。先确认 SQL Server 服务是否运行连接字符串里的服务器名和端口是否正确项目是否以正确环境启动。第三步看依赖EF Core 报找不到 DbContext、迁移工具报缺少程序集都是依赖配置问题。检查 NuGet 包是否安装齐全Program.cs是否注册了 DbContext启动项目是否包含迁移程序集。第四步看参数接口能通但结果不对可能出在参数传递、路由约束或数据校验上。比如[FromBody]和[FromQuery]的使用是否匹配[ApiController]是否让模型验证自动拦截了请求。第五步看日志和内部状态如果以上都正常打开日志看异常堆栈。常见的循环引用、数据库约束冲突、空引用都会在日志里暴露出来。第六步看工具边界某些功能可能不是代码问题而是工具本身不支持。比如早期版本的 EF Core 对某些 SQL Server 功能支持不完整或者 SQL Server 版本过低导致加密协议不匹配。这时候可以查官方文档或升级版本。第七步隔离最小复现如果还是找不到问题尝试从项目里复制出一份最小复现项目只保留一个 Controller、一个实体、一个 DbContext。通常这个过程会很快定位是业务代码问题还是框架配置问题。这个排查链路在真实项目里非常有效因为它强迫你不要跳过环境直接改代码也不要在没有日志的情况下瞎猜。6.3 判断这套方案适不适合你的项目用 .NET 10 Controllers EF Core SQL Server DTOs 是一种非常经典的主流程方案适合很多中小型业务系统、内部管理系统和 B 端 API。但它不是万能的。适合的场景CRUD 为主的业务接口。团队已经有 .NET 技术栈。业务复杂度属于中等实体关系不算特别复杂。需要快速交付且团队愿意遵守分层约定。不适合的场景超大并发、极低延迟的接口可能需要更细粒度缓存和读写分离EF Core 默认链路不一定最合适。非常复杂的领域逻辑比如强业务规则、多步骤事务、事件驱动单纯 Controller DbContext 会容易被写坏。数据库是 PostgreSQL 或 MySQL项目名称里虽然说的是 SQL Server但 EF Core 换 Provider 也能工作只是这套示例的配置要改。团队没有数据库迁移意识直接改生产库那不管什么框架都会出问题。我的建议是先按这套方案跑通最小闭环再根据业务复杂度逐步引入仓储模式、CQRS、事件总线或更完善的映射管理。不要一开始就上重架构因为重架构意味着更多的抽象和更陡的维护成本。回到开头那句话dotnet new webapi只能给你一个能编译的壳真正决定项目能走多远的是你能不能把 Controllers、EF Core、SQL Server、DTOs 组织成一个可维护、可排查、可演进的系统。先跑通最小闭环再补工程化能力最后按业务需要逐步扩展。这条路听着不够酷但它是多数真实项目最稳妥的起跑方式。
返回列表