.NET 10 Web API实战:EF Core + SQL Server + DTO 从零搭建指南
如果你最近准备上手 .NET或者正想把一个 .NET 6 / .NET 8 的老项目升级到 .NET 10那么“Web API EF Core SQL Server DTO”这条主线几乎是绕不开的。很多初学者在接触 .NET 时会被项目模板、ORM、数据库连接和各种分层概念搞得一头雾水网上资料也比较零散。本文用一条完整的示例把从创建 .NET 10 Web API 项目、配置 SQL Server、编写 EF Core 实体与迁移到设计 DTO、实现 Controller 接口并验证的完整流程串起来最后再补充一些开发中高频踩坑的场景。跟着走完你就能从零搭出一套可运行的 Web API 服务。1. 背景与核心概念1.1 为什么要从零搭建一套 .NET 10 Web API在实际业务开发里Web API 是前后端分离架构的服务端载体。前端、移动端、第三方系统都通过 HTTP 接口与后端通信。而 .NET 10 作为微软当前主推的长期支持版本在性能、云原生支持、开发体验上都有明显提升很多新项目都开始基于 .NET 10 规划。从零搭建的意义不是简单执行一条dotnet new命令而是要理解每个环节发生了什么项目模板生成了哪些文件和默认配置。数据库上下文是如何被注入到 Controller 中的。实体、DTO、数据库表三者之间是什么关系。一次请求从进入 Controller 到返回 JSON 的完整路径。当你把这些都弄明白后再去看网上各种复杂架构、整洁架构、微服务示例就会轻松很多。这篇文章的核心目标就是帮你把这条主路径跑通。1.2 本文适合哪些读者这篇文章适合下面几类读者刚接触 .NET 的后端初学者想找一个能一次跑通的完整示例。有 .NET Framework 经验但还不熟悉 .NET 6 现代开发的开发者。需要在本地快速搭建 API 原型用于业务验证或技术预研的同学。已经用 Minimal API 写过小接口想了解 Controller 风格如何组织的开发者。无论你是用 Windows 还是 macOS / Linux只要安装了 .NET SDK都能跟着本文操作。数据库部分我会同时照顾到 SQL Server 和 LocalDB 两种常见场景。1.3 技术选型思路本文采用如下技术组合.NET 10目标框架统一了 Web API 开发体验。Controller 风格 API适合需要分层、团队协作、复杂业务逻辑的项目。EF Core微软官方的 ORM用于操作 SQL Server 数据库。SQL ServerWindows 环境下最常见的关系型数据库本文默认使用 LocalDB 或本地 SQL Server 实例。DTOs数据传输对象用来隔离数据库实体与 API 对外契约。这套组合的最大优势是生态成熟、社区资料多、排错路径清晰。如果你后续要在项目中接入认证、权限、日志、缓存都有非常成熟的中间件可用。2. 技术栈与核心概念解析2.1 .NET 10 与 Web API 的关系.NET 10 是 .NET 统一平台的最新长期支持版本它包含运行时、基础类库和开发工具。Web API 是构建 HTTP 服务的一种方式在 .NET 中通常通过 ASP.NET Core 实现。你可以把 .NET 10 理解为一个底座Web API 是搭建在底座上的一层服务。创建项目时SDK 会生成一个可运行的模板然后我们在这个模板上叠加 EF Core、SQL Server、DTO、业务逻辑等模块。值得强调的是.NET 中同时存在 Controller 风格和 Minimal API 风格两套 API 写法。如果项目规模较小、端点比较少Minimal API 更简洁如果项目有清晰的分层、复杂模型验证、需要遵循传统 MVC 思路Controller 风格更容易维护。本文标题明确要求 Controller因此后续都使用传统控制器方式。2.2 EF Core 与 SQL Server 的配合方式EF Core 是一个对象关系映射框架可以理解为数据库表的 C# 化操作入口。使用 EF Core 时我们不需要手动写大量 SQL而是通过DbContext和DbSetT操作实体EF Core 会在背后生成 SQL 并执行。与 SQL Server 配合时EF Core 通过UseSqlServer()方法指定数据库提供程序并读取连接字符串。开发时可以使用 Windows 身份验证生产环境通常改为 SQL Server 账号密码或托管身份。EF Core 会负责把 LINQ 查询转换为 SQL 查询。跟踪实体状态执行插入、更新、删除。通过迁移机制维护数据库结构。2.3 DTOs 是什么为什么不能直接把实体抛给前端DTOData Transfer Object是数据传输对象用来在进程边界之间传递数据。在 Web API 中DTO 就是 API 对外暴露的请求模型和响应模型。很多新手会直接把 EF Core 的实体类作为 API 的返回类型短期看很方便但项目一复杂就会出问题数据库实体的字段往往比前端需要的更多可能包含内部状态、审计字段、外键关联等。直接暴露实体可能导致“过度提交”用户可以在请求体中传入你不想更新的字段。数据库模型结构变动会直接影响到接口契约导致前端被迫跟着改。如果实体之间存在导航属性序列化时很容易产生循环引用。引入 DTO 后API 层只依赖 DTO不再直接暴露数据库结构。数据库表怎么做调整只要 DTO 不变客户端就无感知。这也是大型项目中最基础的解耦思路之一。3. 环境准备与项目初始化3.1 系统与工具说明本文示例以常见开发环境为例你不需要完全一致的配置重点是掌握操作思路。操作系统Windows 10/11或 macOS / Linux。开发工具Visual Studio 2022或 VS Code C# 扩展或直接使用命令行。运行时.NET 10 SDK。数据库SQL Server 2022 / SQL Server Express或 SQL Server LocalDB。数据库管理工具SQL Server Management StudioSSMS可选。如果你使用 macOS 或 Linux需要额外注意SQL Server LocalDB 只能在 Windows 上安装跨平台开发时建议使用 Docker 启动一个 SQL Server 容器或者连接远程 SQL Server 实例。Docker 启动方式会在后文提及。3.2 安装 .NET SDK打开 .NET 官方下载页面下载对应操作系统的 .NET 10 SDK 并安装。安装完成后在终端执行dotnet --version如果输出类似10.0.x的信息说明 SDK 安装成功。dotnet --list-sdks这个命令可以查看本机安装的所有 .NET SDK 版本。建议保持 SDK 与项目目标框架一致避免后续出现运行时兼容问题。3.3 创建项目打开终端进入你想要创建项目的目录执行dotnet new webapi -n TodoApi --use-controllers cd TodoApi参数说明-n TodoApi指定项目名称为 TodoApi。--use-controllers让模板生成 Controller 风格的 API而不是默认的 Minimal API。执行完成后项目目录下会生成一个最基础的 Web API 工程。如果你使用 Visual Studio也可以直接在创建项目时选择“ASP.NET Core Web API”模板并在创建界面勾选“使用控制器”。模板默认会生成一个WeatherForecast示例控制器。为了保持示例干净后续可以直接删除它或者新建自己的控制器。3.4 添加 EF Core 相关 NuGet 包在项目根目录执行以下命令安装 EF Core SQL Server 提供程序和相关工具包dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Design dotnet add package Microsoft.EntityFrameworkCore.Tools这里有三点需要理解Microsoft.EntityFrameworkCore.SqlServerEF Core 连接 SQL Server 的数据库提供程序必须安装。Microsoft.EntityFrameworkCore.Design提供设计时支持用于执行迁移命令。Microsoft.EntityFrameworkCore.Tools提供 Visual Studio 包管理器控制台里的 EF Core 命令。如果你使用的是 Visual Studio也可以通过“管理 NuGet 程序包”界面搜索安装效果相同。版本号建议与当前 .NET 版本匹配如果使用的是 .NET 8 / .NET 9则选择对应版本的 EF Core 包。3.5 项目结构规划为了让后续代码更清晰我们提前规划好目录结构TodoApi/ ├── Controllers/ # 存放 API 控制器 ├── DTOs/ # 存放数据传输对象 ├── Models/ # 存放数据库实体 ├── Data/ # 存放 DbContext 和数据库相关配置 ├── Mappers/ # 存放实体与 DTO 的映射逻辑 ├── Program.cs # 应用入口和依赖注入配置 ├── appsettings.json # 配置文件 └── TodoApi.csproj # 项目文件你可以手动创建Controllers、DTOs、Models、Data、Mappers这些文件夹也可以继续使用模板自动生成的目录。目录结构本身不是硬性要求但清晰的结构能减少后期维护成本。4. 数据模型与数据库上下文4.1 定义实体类我们以一个简单的待办事项TodoItem作为业务模型。在Models文件夹下新建TodoItem.cs// 文件路径TodoApi/Models/TodoItem.cs namespace TodoApi.Models; public class TodoItem { public int Id { get; set; } public string Title { get; set; } string.Empty; public string? Description { get; set; } public bool IsCompleted { get; set; } public DateTime CreatedAt { get; set; } DateTime.UtcNow; }实体类中的每个属性最终都会映射为数据库表中的一列。Id主键默认自增。Title待办事项标题不能为空。Description备注可空。IsCompleted是否完成。CreatedAt创建时间使用 UTC 时间避免时区问题。这里的string?表示可空字符串 string.Empty则是给非空属性一个安全默认值。4.2 创建 DbContext在Data文件夹下新建AppDbContext.cs// 文件路径TodoApi/Data/AppDbContext.cs using Microsoft.EntityFrameworkCore; using TodoApi.Models; namespace TodoApi.Data; public class AppDbContext : DbContext { public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } public DbSetTodoItem TodoItems SetTodoItem(); }DbContext是 EF Core 的核心类负责协调实体对象与数据库表之间的映射。DbSetTodoItem表示操作TodoItems表的入口我们可以在 Controller 中通过它执行增删改查。4.3 配置连接字符串打开appsettings.json加入连接字符串{ ConnectionStrings: { DefaultConnection: Server(localdb)\\MSSQLLocalDB;DatabaseTodoDb;Trusted_ConnectionTrue;MultipleActiveResultSetstrue }, Logging: { LogLevel: { Default: Information, Microsoft.AspNetCore: Warning } }, AllowedHosts: * }上面连接字符串中的(localdb)\MSSQLLocalDB是 Visual Studio 自带的 LocalDB 实例适合本地开发。如果你使用的是完整的 SQL Server 服务可以把连接字符串改成DefaultConnection: Serverlocalhost;DatabaseTodoDb;Trusted_ConnectionTrue;TrustServerCertificateTrue;MultipleActiveResultSetstrue注意TrustServerCertificateTrue是为了避免本地开发时因证书问题导致连接失败。生产环境应该使用正式的 TLS 加密方式。4.4 使用迁移创建数据库EF Core 的迁移机制可以把 C# 实体定义转换成数据库表结构。首次使用需要先安装dotnet-ef全局工具dotnet tool install --global dotnet-ef然后执行dotnet ef migrations add InitialCreate这个命令会生成一个 Migrations 文件夹里面记录了数据库结构的变化。接着执行dotnet ef database update该命令会根据迁移记录在 SQL Server 中创建TodoDb数据库和TodoItems表。如果一切正常说明数据库准备完成。如果dotnet ef命令提示不存在通常是环境变量没有配置全局工具路径重启终端或手动把%USERPROFILE%\.dotnet\tools加入PATH。5. DTO 设计与映射5.1 为什么要做输入 DTO 和输出 DTO 分离在接口设计中我通常会把 DTO 分成两类请求 DTOInput DTO客户端传给服务端的数据例如创建和更新时提交的内容。响应 DTOOutput DTO服务端返回给客户端的数据包含 Id、创建时间等信息。这么做的好处是客户端不需要知道数据库实体长什么样服务端也只需要接收业务上允许的字段。例如更新待办事项时我们不希望客户端直接修改CreatedAt所以更新 DTO 就不包含这个属性从接口层面就堵住了这种操作。5.2 创建 DTO 类在DTOs文件夹下新建TodoDtos.cs// 文件路径TodoApi/DTOs/TodoDtos.cs using System.ComponentModel.DataAnnotations; namespace TodoApi.DTOs; public class TodoItemDto { public int Id { get; set; } public string Title { get; set; } string.Empty; public string? Description { get; set; } public bool IsCompleted { get; set; } public DateTime CreatedAt { get; set; } } public class CreateTodoItemDto { [Required] [StringLength(200)] public string Title { get; set; } string.Empty; public string? Description { get; set; } public bool IsCompleted { get; set; } } public class UpdateTodoItemDto { [Required] [StringLength(200)] public string Title { get; set; } string.Empty; public string? Description { get; set; } public bool IsCompleted { get; set; } }这里使用 Data Annotations 做基础校验[Required]表示字段不能为空。[StringLength(200)]限制标题最大长度为 200 个字符。如果校验不通过模型绑定会自动返回 400 状态码无需手动判断。5.3 手动映射与 AutoMapper 的取舍实体与 DTO 之间需要转换。最直接的方式是手动映射var dto new TodoItemDto { Id entity.Id, Title entity.Title, Description entity.Description, IsCompleted entity.IsCompleted, CreatedAt entity.CreatedAt };这种方式代码清晰、可控性强缺点是字段多时比较繁琐。另一种方案是使用 AutoMapper 或 Mapster它们能自动映射同名属性减少样板代码但映射规则会变得隐式新手排查问题时往往无从下手。我的建议是项目初期或 DTO 字段简单时优先手动映射。等字段变多、映射规则复杂后再考虑引入 AutoMapper。不要一开始就塞一堆工具增加项目复杂度。5.4 封装统一映射逻辑为了让 Controller 保持简洁可以单独建一个静态映射类。在Mappers文件夹下新建TodoMapper.cs// 文件路径TodoApi/Mappers/TodoMapper.cs using TodoApi.DTOs; using TodoApi.Models; namespace TodoApi.Mappers; public static class TodoMapper { public static TodoItemDto ToDto(TodoItem item) { return new TodoItemDto { Id item.Id, Title item.Title, Description item.Description, IsCompleted item.IsCompleted, CreatedAt item.CreatedAt }; } public static TodoItem ToEntity(CreateTodoItemDto dto) { return new TodoItem { Title dto.Title, Description dto.Description, IsCompleted dto.IsCompleted }; } }这样在 Controller 中一行代码就能完成映射var dto TodoMapper.ToDto(entity);6. 控制器实现与接口设计6.1 创建 TodoController在Controllers文件夹下新建TodoController.cs。首先搭建基础的控制器骨架// 文件路径TodoApi/Controllers/TodoController.cs using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using TodoApi.Data; using TodoApi.DTOs; using TodoApi.Mappers; using TodoApi.Models; namespace TodoApi.Controllers; [ApiController] [Route(api/[controller])] public class TodoController : ControllerBase { private readonly AppDbContext _context; public TodoController(AppDbContext context) { _context context; } }说明[ApiController]让控制器自动启用模型验证、错误响应格式化等能力。[Route(api/[controller])]根据控制器名称自动生成路由TodoController对应/api/Todo。AppDbContext通过构造器注入由 ASP.NET Core 依赖注入容器自动创建。6.2 GET 查询接口返回列表和单个详情查询全部待办事项[HttpGet] public async TaskActionResultIEnumerableTodoItemDto GetAll() { var items await _context.TodoItems .AsNoTracking() .OrderByDescending(x x.CreatedAt) .Select(x new TodoItemDto { Id x.Id, Title x.Title, Description x.Description, IsCompleted x.IsCompleted, CreatedAt x.CreatedAt }) .ToListAsync(); return Ok(items); }查询单个待办事项[HttpGet({id:int})] public async TaskActionResultTodoItemDto GetById(int id) { var item await _context.TodoItems .AsNoTracking() .FirstOrDefaultAsync(x x.Id id); if (item is null) { return NotFound(); } return Ok(TodoMapper.ToDto(item)); }这里的重点AsNoTracking()表示查询结果不需要被 EF Core 跟踪适合只读场景能减少内存开销。async/await避免线程池阻塞提升并发能力。查询不到资源时返回404 NotFound()这是 REST 接口最基本的状态码规范。6.3 POST 创建接口[HttpPost] public async TaskActionResultTodoItemDto Create([FromBody] CreateTodoItemDto dto) { var entity TodoMapper.ToEntity(dto); _context.TodoItems.Add(entity); await _context.SaveChangesAsync(); var result TodoMapper.ToDto(entity); return CreatedAtAction(nameof(GetById), new { id result.Id }, result); }SaveChangesAsync会将实体插入数据库并自动填充自增主键Id。返回CreatedAtAction会产生201 Created响应并在响应头的Location中带上新资源的访问地址。这是 REST 接口里最推荐的创建返回方式。6.4 PUT 更新接口[HttpPut({id:int})] public async TaskIActionResult Update(int id, [FromBody] UpdateTodoItemDto dto) { var entity await _context.TodoItems.FirstOrDefaultAsync(x x.Id id); if (entity is null) { return NotFound(); } entity.Title dto.Title; entity.Description dto.Description; entity.IsCompleted dto.IsCompleted; await _context.SaveChangesAsync(); return NoContent(); }这里先从数据库取出旧实体再把 DTO 中允许修改的字段赋给它最后保存。返回204 No Content表示更新成功但不返回数据。这样设计的好处是客户端不需要关心返回体里的内容只需要判断状态码。6.5 DELETE 删除接口[HttpDelete({id:int})] public async TaskIActionResult Delete(int id) { var entity await _context.TodoItems.FirstOrDefaultAsync(x x.Id id); if (entity is null) { return NotFound(); } _context.TodoItems.Remove(entity); await _context.SaveChangesAsync(); return NoContent(); }删除接口同样遵循先查再删的逻辑避免对不存在的资源执行无效操作。删除完成后返回204 No Content。6.6 数据验证与 HTTP 状态码约定由于[ApiController]会自动执行模型验证所以当客户端的Title为空或超过长度限制时接口会自动返回400 Bad Request。我们不需要在每个方法里都写if (!ModelState.IsValid)。Controller 风格接口中常用的状态码总结如下状态码含义使用场景200 OK查询成功GET 返回数据201 Created资源创建成功POST 创建数据204 No Content操作成功但无返回体PUT 更新、DELETE 删除400 Bad Request请求参数或模型校验失败POST/PUT 入参不合法404 Not Found资源不存在GET/PUT/DELETE 找不到目标保持一致的状态码语义能让接口更规范也方便前端统一处理错误。7. 配置 Program.cs 并运行验证7.1 修改 Program.cs在默认模板中Program.cs 只包含最基础的中间件配置。我们需要把 DbContext 注册进去。打开Program.cs修改为// 文件路径TodoApi/Program.cs using Microsoft.EntityFrameworkCore; using TodoApi.Data; var builder WebApplication.CreateBuilder(args); // 注册控制器服务 builder.Services.AddControllers(); // 注册 EF Core DbContext使用 SQL Server builder.Services.AddDbContextAppDbContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(DefaultConnection))); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();关键点AddControllers()注册控制器相关的 MVC 服务。AddDbContext把AppDbContext注册为作用域服务每个请求会创建一个实例。UseSqlServer从配置文件中读取连接字符串。MapControllers()将所有 Controller 中的路由端点映射到请求管道。7.2 启动 API 并用 Swagger 测试执行dotnet run启动成功后终端会输出本地监听地址默认是https://localhost:5001或http://localhost:5000。打开浏览器访问https://localhost:5001/swagger就能看到 Swagger UI 页面。Swagger 是 ASP.NET Core 内置的接口文档和调试工具。在面板中你可以直接看到GET /api/Todo、POST /api/Todo等接口点击“Try it out”即可模拟请求。建议按以下顺序验证完整流程执行GET /api/Todo返回一个空数组。执行POST /api/Todo创建一个待办事项返回201和新建对象。执行GET /api/Todo/{id}查看刚才创建的数据。执行PUT /api/Todo/{id}修改标题返回204。执行DELETE /api/Todo/{id}删除数据返回204。7.3 检查数据库结果如果你安装了 SQL Server Management StudioSSMS可以连接到(localdb)\MSSQLLocalDB或localhost打开TodoDb数据库查看TodoItems表。每执行一次添加或更新操作都可以看到对应的数据变化。如果不想安装图形工具也可以使用 SQL Server 自带的sqlcmd命令行工具。连接方式有多种具体命令会因版本和环境不同而有差异建议在 SQL Server 官方文档中查找对应版本的使用方法。8. 常见问题与排查思路8.1 dotnet ef 命令找不到在终端执行dotnet ef时如果提示“不是内部或外部命令”或者command not found说明没有安装全局 EF Core 工具。解决方案dotnet tool install --global dotnet-ef安装完成后重启终端。如果仍然找不到需要把%USERPROFILE%\.dotnet\tools添加到系统PATH环境变量中。8.2 连接 SQL Server 失败使用 EF Core 执行迁移或运行时最常见的错误是“无法连接到数据库”。排查顺序如下确认 SQL Server 服务已启动。在 Windows 上打开“服务”管理器找到SQL Server (MSSQLSERVER)确认状态为“正在运行”。确认连接字符串中的服务器地址正确。LocalDB 使用(localdb)\MSSQLLocalDB本地实例使用localhost。确认防火墙是否允许 1433 端口尤其是连接远程 SQL Server 时。如果使用 SQL Server 账号登录确认用户名和密码正确并具备访问目标数据库的权限。本地开发时如果遇到证书相关报错可以在连接字符串中加入TrustServerCertificateTrue。8.3 SQL Server 安装和启动时的常见错误不少读者在安装 SQL Server 或启动服务时会遇到各种问题这里援引几种常见现象问题现象常见原因解决思路安装时提示 waiting on the database engine recovery handle failed安装账号权限不足、已存在冲突实例、磁盘异常查看 SQL Server 错误日志和 Windows 事件查看器SQL Server 服务远程调用失败服务未启动或配置异常重启服务检查服务账户权限ODBC 驱动连接报 named pipes provider 错误Named Pipes 协议未启用或客户端连接方式不匹配使用 SQL Server Configuration Manager 启用协议改用 TCP/IP 连接SQL Server 安装过程相对繁琐建议在干净环境中安装避免残留旧版本导致冲突。生产环境中遇到这类问题优先查看 SQL Server 日志目录下的 ERRORLOG 文件。8.4 迁移完成后接口报“Invalid column name”修改实体类后如果没有生成新的迁移数据库表结构保持不变EF Core 查询时就会报“列名无效”。解决方案dotnet ef migrations add AddNewField dotnet ef database update记住每次增加或删除实体中的字段都要执行一次迁移。开发环境可以频繁更新数据库生产环境推荐先生成 SQL 脚本由 DBA 审核后再执行。8.5 项目使用 Minimal API 模板没有 Controllers如果你创建时没有使用--use-controllers项目默认是 Minimal API 结构文件中只有 Program.cs 和MapGet之类的方法。解决方案有两种重新用dotnet new webapi --use-controllers创建项目。手动加入Controllers文件夹创建 Controller 类并在 Program.cs 中调用AddControllers()和MapControllers()。如果只是建一个简单的小接口继续用 Minimal API 也可以但本文的代码都建立在 Controller 风格之上。9. 最佳实践与工程建议9.1 配置管理不要把连接字符串写死在代码里开发环境可以使用appsettings.Development.json本机调试时用 LocalDB 或本机 SQL Server。生产环境建议通过环境变量或密钥管理服务注入连接字符串不要提交到 Git 仓库。使用 user-secrets 也可以管理开发环境敏感信息dotnet user-secrets init dotnet user-secrets set ConnectionStrings:DefaultConnection Serveryour-server;DatabaseTodoDb;User Idapp_user;Passwordyour-password;生产环境优先使用环境变量例如在 Linux 上设置export ConnectionStrings__DefaultConnectionServer...;Database...;User Id...;Password...;注意ASP.NET Core 配置系统会自动把__解析为配置层级。9.2 数据库账号最小权限不要在连接字符串中使用sa或dba这类高权限账号。给应用程序单独创建一个数据库账号只授予目标数据库的读写权限。这样即使应用被入侵也无法操作其他数据库。最小权限原则同样适用于生产环境数据库变更。EF Core 的自动迁移功能适合开发环境生产环境建议生成迁移脚本由 DBA 审核后手动执行。9.3 异步与性能优化EF Core 的查询、保存方法都有对应的异步版本ToListAsync()FirstOrDefaultAsync()SaveChangesAsync()AnyAsync()CountAsync()在 Controller 方法中只要涉及数据库操作一律使用async方法与await避免线程池线程被阻塞。对于只需要读取、不修改的查询加上AsNoTracking()可以减少状态跟踪的开销。列表接口在数据量大时还需要实现分页参数而不是一次性把所有数据都查出来。9.4 日志与异常处理使用内置的ILoggerT记录关键操作和异常。例如private readonly ILoggerTodoController _logger; public TodoController(AppDbContext context, ILoggerTodoController logger) { _context context; _logger logger; }捕获异常时不要把堆栈信息直接返回给客户端。生产环境应该记录完整异常到日志中心返回给客户端的只需要一个友好的错误提示。可以在项目中增加全局异常过滤器集中处理未捕获异常。9.5 接口契约与版本管理当 API 对外提供服务后DTO 字段的变化会影响客户端。如果字段要改名、删除或改变语义建议增加接口版本控制例如/api/v1/todo、/api/v2/todo避免强制所有客户端同步升级。在团队协作中DTO 字段尽量保持只增不减。新增字段时给可空类型或默认值能让老版本客户端不受影响。9.6 测试与验证实体映射、DTO 校验、Controller 的 CRUD 逻辑建议补充基础单元测试和集成测试。尤其是TodoMapper这类纯映射逻辑测试成本很低收益却很直接。集成测试可以基于真实数据库或内存数据库验证接口返回状态码和数据结构。9.7 实体变更与迁移策略每次修改实体后尽快生成对应的迁移文件并把迁移文件一起提交到代码仓库。不要把迁移文件忽略掉。这样团队其他人拉取代码后执行一次dotnet ef database update就能同步数据库结构。如果公司有专门的 DBA 负责数据库变更可以生成 SQL 脚本dotnet ef migrations script然后把脚本交给 DBA 审核而不是在生产环境直接执行database update。10. 总结与实践建议到这里一个基于 .NET 10 的 Controller 风格 Web API 已经从零跑通了。我们完成了项目创建、EF Core 实体建模、SQL Server 数据库迁移、DTO 分层、Controller 的增删改查实现以及 Swagger 接口验证。这套结构可以直接扩展到实际业务中不管是做待办事项、用户管理、订单系统还是更复杂的业务模型核心流程都是一样的。下一步可以继续学习的方向包括给接口加入 JWT 认证和基于角色的权限控制。使用全局异常过滤器和自定义响应格式。引入分页、筛选、排序提升列表接口能力。使用 Mapster 或 AutoMapper 优化实体与 DTO 转换。把项目拆分为 Service 层和 Repository 层实现更彻底的职责分离。使用 Docker 启动 SQL Server统一团队开发环境。在日常开发中我更建议你先跑通一条最小可用的链路再逐步叠加复杂功能。很多项目失败不是因为技术选型不对而是没有统一好 SDK、数据库和接口规范。只要把基础打牢后续扩展都是水到渠成的事情。如果这篇教程对你有帮助可以收藏备用也欢迎在实际踩坑后回来对照排查清单。