ASP.NET Core 10 Minimal APIs 实战:轻量 API 开发与性能观察

发布时间:2026/9/4 6:58:49
ASP.NET Core 10 Minimal APIs 实战:轻量 API 开发与性能观察 开始之前先给结论Minimal APIs 不是玩具也不是只能写 demo 的边角功能。在 ASP.NET Core 10 这一代它已经是能支撑微服务、工具型 API、Webhook、后台任务入口的正式推荐方案。相比传统 Controller APIMinimal APIs 的代码更短、启动更快、路由表达更直接而且从项目创建到 OpenAPI 文档生成整条链路都能在 10 分钟内跑通。这篇文章就带着你从头跑一遍安装 .NET 10 SDK、创建项目、编写 GET/POST/PUT/DELETE 接口、开启 OpenAPI 文档、做批量导入接口再用 curl 和 Python 调用验证。如果你之前只写过传统的 ASP.NET Core ControllerMinimal APIs 可以看成同一种后台能力的另一套写法。它没有推翻依赖注入、配置系统、中间件管线这些 ASP.NET Core 底层设计而是取消掉了 Controller、Action、Attribute 带来的大量样板代码把“HTTP 路径”和“C# 方法”直接对应起来。这种写法对小型服务非常友好一个 Program.cs 就是完整应用少了一层目录结构少了类之间的跳转阅读和维护成本都低不少。这次我们不会停在“能启动”层面还会把几个经常被忽略的问题一起讲清楚Minimal API 的参数绑定规则是什么复杂类型从 JSON Body 来简单类型从路由或查询字符串来这一点直接影响接口能不能被正确调用。还有 OpenAPI 文档怎么生成、批量写入接口怎么做数量限制、服务启动后内存和线程状态怎么观察、发布到生产环境时该用普通发布还是 Native AOT。这些都是实际开发中会遇到的真实问题。适合看这篇文章的读者有三类第一是刚入门 .NET 后端想找一套轻量 API 开发模式的 C# 开发者第二是在维护老 Controller 项目考虑把新增接口改成 Minimal API 的技术负责人第三是做小工具、内部服务、脚本调用入口想少写大量装饰器代码的开发者。下面直接进入正题。1. Minimal APIs 核心能力速览能力项说明所属框架ASP.NET Core 10.NET 10 SDK 内置API 模式Minimal APIs自 .NET 6 引入在 .NET 10 中已成熟项目形态单个 Program.cs 或按模块拆分没有 Controller 目录启动方式dotnet run也可以在 Visual Studio / VS Code / Rider 中直接调试路由能力MapGet、MapPost、MapPut、MapDelete、MapGroup支持路由参数约束依赖注入路由处理器参数直接注入服务不需要构造函数OpenAPI 支持通过AddOpenApi()和MapOpenApi()生成 JSON 文档批量任务没有内置队列但可以快速实现批量导入接口或结合 BackgroundService 消费任务混合使用同一个应用中可同时存在 Minimal API 与 Controller API适用平台Windows / Linux / macOS支持 Docker 部署典型场景微服务、Webhook、小工具 API、后台管理系统接口、演示项目说明一点上表中的“批量任务”和“支持 API”指的是框架能力边界。Minimal APIs 属于 Web 框架不内置消息队列任务队列通常需要你自己定义并发模型或者引入 Channel、Hangfire、Quartz 这类组件。后面会给出一个可运行的批量导入接口示例你可以在此基础上扩展成真正的后台队列。2. Minimal APIs 使用场景与适用边界2.1 什么场景适合 Minimal APIMinimal API 最适合的是“接口数量有限但希望接口足够清晰”的应用。典型场景包括前后端分离项目中的小模块 API、定时任务管理系统中的任务提交接口、Webhook 接收接口、内部运维工具接口以及以 CRUD 为主的简单业务系统。这类项目如果用 Controller会感觉每个 Action 都是一层包装创建控制器类、声明路由、声明 HttpGet/HttpPost、再用构造函数注入服务真正业务代码只有几行而周边样板有几十行。模块化方面Minimal API 并不弱。ASP.NET Core 8 之后已经加入了MapGroup可以把一组路由组织成组统一配置前缀、标签、过滤器。例如var todoApi app.MapGroup(/api/items) .WithTags(TodoItems); todoApi.MapGet(/, (ItemRepository repo) ...); todoApi.MapPost(/, (TodoCreateRequest input, ItemRepository repo) ...);这样代码结构仍然能按业务拆开放在不同的静态类或扩展方法中不会因为用了 Minimal API 就让所有接口挤在同一个文件里。2.2 什么场景不建议用 Minimal API不建议用 Minimal API 的情况也要说清楚。如果你维护的是一个大型 ERP 或中台系统接口数量几百上千并且团队已经养成了 Controller Service Repository 的分层习惯那么整个团队继续使用 Controller 可能是更好的选择。原因不是 Minimal API 能力不够而是团队协作时统一模式往往比单接口代码量更重要。Controller 路由和授权特性已经形成了一套强约定新人迁移成本低代码检索也方便。另外如果你需要大量使用继承、拦截器、统一模型绑定逻辑等强框架特性Controller 基类提供的封装能力仍然有优势。Minimal API 不是“取代” Controller而是“另一种更轻的选项”。工程选型不应该只看 Demo 有多简洁还要看项目生命周期里的维护成本。2.3 使用边界与合规提醒从技术上Minimal API 可以构建公开的数据接口也可以构建处理用户信息、上传文件、人脸图片等服务。涉及处理个人数据、用户文件或第三方内容时必须在接口设计阶段考虑授权、日志、访问限流和隐私保护要求。不要在公网接口上不做鉴权就开放写入能力不要用示例中的内存仓储处理生产数据也不要对未授权的版权内容、他人肖像、敏感文件做采集或再分发。示例代码请在本地开发环境验证。3. ASP.NET Core 10 本地开发环境准备3.1 安装 .NET 10 SDK在开始写代码之前先确认本机已经安装了 .NET 10 SDK。这里只依赖一个官方运行时不需要额外安装数据库和第三方依赖。Linux、macOS、Windows 的安装方式不同最简单的方式是打开终端用常见包管理器安装。Windows 上可以用 winget 搜索并安装winget search Microsoft.DotNet.SDK从搜索列表中选择对应 .NET 10 的 SDK 包进行安装。macOS 上可以使用 brewbrew install dotnet-sdk-10Linux 上建议使用微软官方 Linux 软件源然后通过 apt 或 dnf 安装。如果你不想折腾包管理器也可以直接去 Microsoft 官网下载对应系统的 SDK 安装包。安装完成后重启终端让环境变量生效。3.2 验证环境是否可用打开终端执行下面两条命令确认 SDK 安装成功dotnet --version dotnet --list-sdksdotnet --version会输出当前默认的 SDK 版本例如 10.0.100 或类似版本号。dotnet --list-sdks会列出本机安装的全部 SDK。如果这里看不到任何 .NET 10 版本后面的项目创建就会失败需要先解决 SDK 安装问题再继续。接下来再看一下项目模板是否正常。执行dotnet new list输出列表中应该能看到 “ASP.NET Core Web API” 和 “ASP.NET Core Empty” 两个模板它们都可以用来创建 Minimal API 项目。3.3 准备编辑器与可选工具编辑器方面Windows 下可以用 Visual Studio 最新稳定版装好ASP.NET 和 Web 开发工作负载跨平台开发更推荐 Visual Studio Code 搭配 C# Dev Kit 扩展或者使用 JetBrains Rider。这些工具都支持直接打开项目运行调试。如果需要观察接口性能和资源占用可以额外安装 dotnet-counters 工具dotnet tool install --global dotnet-counters这个工具后面在第 7 章会用到主要是用来查看进程的 CPU、内存、线程池和 GC 状态不需要写任何额外代码。4. 创建 Minimal API 项目与启动服务4.1 创建项目我习惯用 Empty 模板创建项目因为这样生成的代码最干净没有大量多余的模板注释。打开终端执行dotnet new web -n Todo.MinimalApi cd Todo.MinimalApi执行完后目录里会包含一个 Todo.MinimalApi.csproj 和 Program.cs。此时项目已经是一个能运行的 Minimal API 应用但默认只有一个根路由。我们先添加 OpenAPI 支持dotnet add package Microsoft.AspNetCore.OpenApi这个包用于生成 OpenAPI 文档。如果网络环境访问 NuGet 比较慢可以确认一下 NuGet 源配置国内开发者也可以把 nuget.org 源和镜像源的速度做一次对比选择更快的源。4.2 编写完整的示例子代码把默认的 Program.cs 替换成下面的内容。这个示例包含了一个内存版 Todo 仓储以及完整的增删改查接口var builder WebApplication.CreateBuilder(args); builder.Services.AddOpenApi(); builder.Services.AddSingletonItemRepository(); var app builder.Build(); if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } app.MapGet(/, () Results.Ok(new { service ASP.NET Core 10 Minimal API, time DateTimeOffset.Now })); app.MapGet(/api/items, (ItemRepository repo) { var items repo.GetAll(); return Results.Ok(items); }); app.MapGet(/api/items/{id:int}, (int id, ItemRepository repo) { var item repo.GetById(id); return item is null ? Results.NotFound() : Results.Ok(item); }); app.MapPost(/api/items, (TodoCreateRequest input, ItemRepository repo) { if (string.IsNullOrWhiteSpace(input.Title)) { return Results.ValidationProblem(new Dictionarystring, string[] { [title] new[] { title 不能为空 } }); } var created repo.Add(new TodoItem { Title input.Title, IsCompleted input.IsCompleted }); return Results.Created($/api/items/{created.Id}, created); }); app.MapPut(/api/items/{id:int}, (int id, TodoUpdateRequest input, ItemRepository repo) { var updated repo.Update(id, new TodoItem { Id id, Title input.Title, IsCompleted input.IsCompleted }); return updated is null ? Results.NotFound() : Results.Ok(updated); }); app.MapDelete(/api/items/{id:int}, (int id, ItemRepository repo) { return repo.Delete(id) ? Results.NoContent() : Results.NotFound(); }); app.Run(); public class TodoItem { public int Id { get; set; } public string Title { get; set; } string.Empty; public bool IsCompleted { get; set; } } public class TodoCreateRequest { public string Title { get; set; } string.Empty; public bool IsCompleted { get; set; } } public class TodoUpdateRequest { public string Title { get; set; } string.Empty; public bool IsCompleted { get; set; } } public class ItemRepository { private readonly Dictionaryint, TodoItem _items new(); private int _nextId 1; public ListTodoItem GetAll() _items.Values.OrderBy(item item.Id).ToList(); public TodoItem? GetById(int id) _items.TryGetValue(id, out var item) ? item : null; public TodoItem Add(TodoItem item) { item.Id _nextId; _items[item.Id] item; return item; } public TodoItem? Update(int id, TodoItem item) { if (!_items.ContainsKey(id)) { return null; } item.Id id; _items[id] item; return item; } public bool Delete(int id) _items.Remove(id); }这段代码的重点是让你感受 Minimal API 的核心写法MapGet、MapPost、MapPut、MapDelete后面的第一个参数是路由模板第二个参数是路由处理器。路由处理器里需要的服务会通过参数自动注入比如ItemRepository repo就是从依赖注入容器里拿到的单例服务。4.3 启动并验证端口启动项目执行dotnet run如果一切正常终端会输出类似 “Now listening on: http://localhost:5xxx” 的日志。模板可能使用随机端口所以不要死记某个固定端口以你自己的终端输出为准。如果你想让端口固定下来可以把启动命令改成dotnet run --urls http://localhost:5167然后打开浏览器访问http://localhost:5167/如果看到 JSON 输出说明服务已经正常启动。终端里出现监听地址后先不要关闭窗口因为后续的接口测试都依赖这个服务。5. Minimal API 功能测试与效果验证5.1 健康检查路由先测试根路由判断服务是否还在运行curl http://localhost:5167/预期输出类似{service:ASP.NET Core 10 Minimal API,time:2026-01-01T10:00:0008:00}只要能拿到 JSON就说明 .NET 10 运行时、SDK 环境和项目代码都没有问题。如果你看到的是“Connection refused”第一件事是确认服务进程是否还活着第二件事是检查端口是否写对。5.2 创建资源并查询资源接下来向/api/items发送 POST 请求创建一个新条目curl -i -X POST http://localhost:5167/api/items \ -H Content-Type: application/json \ -d {title:写一篇 CSDN 技术博客,isCompleted:false}注意Minimal API 默认使用 JSON 大小写不敏感的属性绑定方式所以请求体里写title或Title都能绑定到TodoCreateRequest.Title。正常情况下会返回201 Created响应头里有Location响应体类似{ id: 1, title: 写一篇 CSDN 技术博客, isCompleted: false }现在用 GET 请求验证数据已经存进内存仓储curl http://localhost:5167/api/items如果刚才创建成功返回列表里应该能看到 id 为 1 的那条记录。继续测试单条查询curl http://localhost:5167/api/items/1 curl -i http://localhost:5167/api/items/999第一条返回刚才创建的对象第二条由于 id 不存在预期返回 404。这种简单验证能帮你确认路由约束{id:int}是否生效以及Results.NotFound()是否按预期工作。5.3 更新与删除资源更新资源curl -i -X PUT http://localhost:5167/api/items/1 \ -H Content-Type: application/json \ -d {title:写一篇 CSDN 技术博客并发布,isCompleted:true}预期返回 200并且响应体中的isCompleted变为 true。然后删除资源curl -i -X DELETE http://localhost:5167/api/items/1预期返回 204 No Content。再查一次单条资源应该变成 404。5.4 参数校验与错误返回Minimal API 没有内置 FluentValidation但可以像示例中那样自己写一个if判断。比如 POST 一个空标题curl -i -X POST http://localhost:5167/api/items \ -H Content-Type: application/json \ -d {title:,isCompleted:false}预期返回 400并且响应体里带有标准问题详情格式其中errors.title会包含我们写的“title 不能为空”。这个细节提示了 Minimal API 的出错返回是可以被前端统一解析的不要只返回裸字符串。6. Minimal API 的 OpenAPI 文档与批量任务接口6.1 启动 OpenAPI JSON 文档在 Program.cs 中我们已经调用了builder.Services.AddOpenApi()和app.MapOpenApi()。服务运行时直接访问curl http://localhost:5167/openapi/v1.json如果能看到一份 JSON 文档说明 OpenAPI 已经生效。这里生产的是 OpenAPI 3.0/3.1 格式的接口描述它本身不是 UI 页面。想要可视化调试界面可以把这份 JSON 地址接到 Scalar、Swagger UI 或 Postman 里。接口每次发生变化时OpenAPI 文档会同步体现在这个 JSON 里不需要额外维护接口文档。6.2 实现批量导入接口现在在 Program.cs 中添加一个新的批量导入路由。它接收一个数组限制单次最大 100 条并返回创建后的完整数据。在app.Run()之前加入如下代码app.MapPost(/api/items/batch, (ListTodoCreateRequest inputs, ItemRepository repo) { if (inputs.Count 100) { return Results.BadRequest(new { error 单次批量最多 100 条 }); } var created new ListTodoItem(inputs.Count); foreach (var input in inputs) { if (string.IsNullOrWhiteSpace(input.Title)) { return Results.ValidationProblem(new Dictionarystring, string[] { [title] new[] { title 不能为空 } }); } created.Add(repo.Add(new TodoItem { Title input.Title, IsCompleted input.IsCompleted })); } return Results.Created(/api/items, created); });这个接口说明了几个生产注意事项批量操作必须加数量上限避免一次请求打爆进程内存或数据库连接。批量接口不是一定比多个单条请求更快真正瓶颈往往在数据库写入和事务处理。如果中途某一条数据校验失败通常建议快速失败并返回明确错误而不是写一半再回滚。测试批量接口curl -i -X POST http://localhost:5167/api/items/batch \ -H Content-Type: application/json \ -d [{title:任务1},{title:任务2},{title:任务3}]预期响应里包含三个创建后的对象状态码为 201。可以在后续开发中把它扩展为异步任务队列模式接口先返回 202 Accepted 和一个 jobId后台再用BackgroundService消费队列中的任务这样就能把耗时的批量处理从 HTTP 请求中剥离出去。6.3 用 Python requests 调用接口Minimal API 本质就是一个普通 HTTP 服务所以任何支持 HTTP 的客户端都可以调用。这里给一个 Python 调用示例方便你在脚本中快速批量写入import requests base_url http://localhost:5167 # 查询现有条目 resp requests.get(f{base_url}/api/items, timeout10) print(GET /api/items, resp.status_code, resp.json()) # 单条创建 resp requests.post( f{base_url}/api/items, json{title: 来自 Python 的任务, isCompleted: False}, timeout10, ) print(POST /api/items, resp.status_code, resp.json()) # 批量创建 payload [ {title: 批量任务-1}, {title: 批量任务-2}, ] resp requests.post( f{base_url}/api/items/batch, jsonpayload, timeout30, ) print(POST /api/items/batch, resp.status_code, resp.json())写脚本时注意设置 timeout不要用默认的无超时方式调用公网接口。批量任务耗时可能比较长可以把 timeout 调大但更好的做法是使用异步任务接口让客户端轮询任务状态。7. 资源占用与性能观察7.1 开发模式下观察服务状态Minimal API 的优势之一是没有 Controller 层的模板代码因此进程启动速度通常更快内存占比更稳定。但具体内存占用、