ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ASP.NET Core 10 Minimal API 从入门到IIS部署实战指南

ASP.NET Core 10 Minimal API 从入门到IIS部署实战指南 在实际开发中很多 HTTP 接口并不需要 Controller、Action 和一大把路由特性。从 .NET 6 开始ASP.NET Core 引入了 Minimal APIs用一条MapGet或MapPost就能把请求映射到处理逻辑上。到了 ASP.NET Core 10这套写法和相关的依赖注入、配置、OpenAPI、IIS 发布链路依然是轻量服务里最常用的组合。接下来的内容按照“先建最小项目、再补齐接口、最后发布到 IIS”的顺序展开核心目标只有一个跑通一个能创建、查询 Todo 的最小 API并把它部署到 Windows Server 的 IIS 上。这篇内容针对已经能写出基础 C# 代码、但对 Minimal API 还不熟悉的开发者。你会看到一个空项目如何变成带 GET、POST、PUT、DELETE 的接口服务也会看到 Minimal API 参数绑定的规则、返回IResult的原因、Swagger 调试方法以及发布到 IIS 后遇到 502.5、500.30、500.35 时从哪里入手排查。1. 先理解 Minimal API 是什么以及为什么要用它1.1 一句话解释 Minimal APIMinimal API 是 ASP.NET Core 提供的一类“轻量接口定义方式”。它允许你直接使用app.MapGet(/path, handler)这类方法把 HTTP 方法、路由路径和一个委托绑定在一起不需要单独建Controller类、不需要继承ControllerBase也不需要让每个 Action 放在一个类里。最小示例就是dotnet new web生成的Program.csvar builder WebApplication.CreateBuilder(args); var app builder.Build(); app.MapGet(/, () Hello World!); app.Run();这段代码已经是一个可运行的 ASP.NET Core 应用。浏览器访问根路径时会返回Hello World!。这里没有“类”的痕迹只有启动、路由和运行三步。1.2 Minimal API 和传统 Controller API 的核心区别传统 Web API 通常长这样[ApiController] [Route(api/[controller])] public class TodosController : ControllerBase { [HttpGet] public IActionResult GetAll() { return Ok(new[] { 学习 Minimal API }); } }Controller 带来的好处是结构固定、约定明确适合大量 Action 的组织。但对小接口来说它需要来回切换文件、添加特性、处理继承样板代码明显更多。Minimal API 的价值不在“消灭类”而在“消除不必要的一层封装”。两者对比可以看下面的表对比项传统 Controller APIMinimal API定义位置Controller 文件中的 ActionProgram.cs或扩展方法中的 Lambda路由声明[Route]、[HttpGet]特性app.MapGet、app.MapPost返回结果ActionResultT/IActionResultT、IResult、匿名对象适合规模中型到大型项目小项目、微服务、后台回调过滤器生态ActionFilter等成熟管道使用中间件和 endpoint filter 补齐模型绑定约定清晰多参数易处理自动绑定参数复杂场景需 DTOMinimal API 并不会让 C# 或 ASP.NET Core 变弱它只是换了一种更直接的表达方式。1.3 什么场景优先选择什么场景不要硬用优先选择 Minimal API 的场景内网服务、后台回调、定时任务暴露的监控接口。一个项目只有十几个或几十个接口不需要复杂约定。想把整个服务尽量压到一个文件里快速验证。学习 ASP.NET Core 路由、依赖注入、中间件时它更容易看清楚请求链路。不要为了用 Minimal API 而把所有大业务硬塞进一个Program.cs。如果项目已经有几十个 Controller、大量过滤器、复杂模型验证和多租户需求保留 Controller 结构更加合理。Minimal API 也可以支持复杂项目但需要自己拆分组、扩展方法和 endpoint filter维护成本不一定比 Controller 低。2. 准备 ASP.NET Core 10 开发环境并创建第一个项目2.1 开发机需要安装什么构建 ASP.NET Core 10 项目时最稳妥的方式是安装与目标版本匹配的 .NET 10 SDK。SDK 里已经包含dotnet命令行、编译器和 ASP.NET Core Runtime不需要额外安装 Visual Studio 也可以创建项目。常用环境要求如下工具用途.NET 10 SDK创建项目、编译、运行、发布Visual Studio 2022 或更高版本可选便于调试VS Code C# Dev Kit可选轻量开发Postman 或 curl调试 HTTP 接口Windows Server IIS部署服务进入项目目录前先确认 SDK 是否可用dotnet --version dotnet --list-sdks如果本机安装过多个 .NET 版本输出里会包含多个 SDK。创建项目时dotnet new会使用当前目录的global.json如果没有则默认使用最新 SDK。2.2 使用 dotnet CLI 创建空项目创建 Minimal API 项目的方式有很多最简单的命令是dotnet new web -n MinimalTodo.Api cd MinimalTodo.Api为什么不直接使用dotnet new webapi因为webapi模板会附带很多与“最小接口”演示无关的内容而web模板生成的是只有一个Program.cs的空 ASP.NET Core 项目非常适合从零讲解 Minimal API。项目创建完成后运行dotnet run正常启动时命令行会输出 Kestrel 监听的地址通常是Now listening on: http://localhost:5214具体端口来自Properties/launchSettings.json。不要直接照抄别人博客里的端口浏览器访问时应该使用自己电脑上实际显示的地址。2.3 默认生成的 Program.cs 为什么这么短dotnet new web默认模板如下var builder WebApplication.CreateBuilder(args); var app builder.Build(); app.MapGet(/, () Hello World!); app.Run();这里有两个关键机制Top-level statementsC# 允许把顶层语句放在文件最前面编译器会生成Program类入口不需要手写Main方法。Implicit usings使用WebApplication.CreateBuilder时常见的Microsoft.AspNetCore.Builder、Microsoft.AspNetCore.Http等命名空间会被隐式引入所以不需要一堆using。builder.Build()负责创建 WebApplicationapp.Run()启动应用并开始接收请求。中间的路由映射代码就是接下来要扩展的位置。3. 实现一个最小 Todo API从查询到创建3.1 先把 GET 接口跑通把默认Program.cs改成返回一个内存中的 Todo 列表var builder WebApplication.CreateBuilder(args); var app builder.Build(); var todos new ListTodo { new Todo(1, 了解 Minimal API, false), new Todo(2, 编写第一个接口, false) }; app.MapGet(/todos, () todos); app.Run(); record Todo(int Id, string Title, bool IsComplete);运行后访问/todos会看到 JSON 数组[ { id: 1, title: 了解 Minimal API, isComplete: false }, { id: 2, title: 编写第一个接口, isComplete: false } ]这里有一个值得注意的设计MapGet(/todos, () todos)返回的是ListTodo框架会默认把返回值序列化为 JSON状态码为 200。你不需要手动调用JsonSerializer.Serialize也不需要自己设置Content-Type。3.2 增加按 Id 查询的单条接口继续添加路由app.MapGet(/todos/{id:int}, (int id) { var todo todos.FirstOrDefault(t t.Id id); return todo is null ? Results.NotFound() : Results.Ok(todo); });路径中的{id:int}是一个路由约束。它表示id必须是整数。请求/todos/1时委托参数int id会自动绑定请求/todos/abc时因为不符合:int约束框架不会执行这个委托通常会返回 404。返回类型从对象变成了IResult。Results.NotFound()返回 404 状态码Results.Ok(todo)返回 200 和 JSON 对象。显式区分“找不到”和“找到”是 HTTP API 的基本要求。3.3 增加 POST 创建接口创建写入需要接收客户端传递的数据。Minimal API 的规则是当委托参数中只有一个复杂类型时框架会默认把它当作请求体 JSON 来反序列化。app.MapPost(/todos, (CreateTodoInput input) { if (string.IsNullOrWhiteSpace(input.Title)) { return Results.BadRequest(new { message Title 不能为空 }); } var nextId todos.Count 0 ? todos.Max(t t.Id) 1 : 1; var todo new Todo(nextId, input.Title, input.IsComplete); todos.Add(todo); return Results.Created($/todos/{todo.Id}, todo); }); app.Run(); record CreateTodoInput(string Title, bool IsComplete); record Todo(int Id, string Title, bool IsComplete);删除之前没有处理额外的情况。写 POST 时要注意两个点不要让客户端指定主键。创建数据的Id应该由服务端生成。接收参数不能直接使用Todo否则客户端传入一个错误的Id可能污染数据。更合理的做法是使用单独请求 DTO。用 curl 验证curl -X POST http://localhost:5214/todos \ -H Content-Type: application/json \ -d {title:写一篇技术博客,isComplete:false}预期返回值是 JSON同时能看到 Location 响应头指向新资源的地址。3.4 补上更新和删除形成完整闭环一个简单 API 最好能覆盖增删改查四个动作app.MapPut(/todos/{id:int}, (int id, CreateTodoInput input) { var index todos.FindIndex(t t.Id id); if (index 0) { return Results.NotFound(); } todos[index] new Todo(id, input.Title, input.IsComplete); return Results.NoContent(); }); app.MapDelete(/todos/{id:int}, (int id) { var removed todos.RemoveAll(t t.Id id); return removed 0 ? Results.NotFound() : Results.NoContent(); });到这里你已经拥有了一个可演示的 Todo API。不过这段代码只适合学习因为内存列表一旦应用重启就会丢失而且多个请求并发操作同一个ListT时也不安全。生产环境要把这里换成数据库并通过异步方法读取数据。4. 继续深入 Minimal API 参数绑定、返回值和路由组织4.1 参数到底从哪里来Minimal API 的委托参数绑定顺序值得单独理清。常见来源有三种参数来源示例说明路由值/todos/{id}参数名与占位符一致时自动绑定Query 参数/todos?keywordabc非路由值、可空、基础类型参数默认从 query 获取请求体POST JSON参数是复杂对象时从 JSON body 反序列化依赖注入TodoService service参数在 DI 容器中有注册时自动解析一个更复杂的例子app.MapGet(/products/{id}, (int id, string? keyword, ProductService service) { return service.GetById(id, keyword); });这里的id来自路由keyword来自 queryservice来自依赖注入容器。为什么要理解这些绑定规则因为出错时最容易被误导。比如参数名id写成productId可能导致路由值绑不上而一直取默认值 0复杂对象出现两个时Minimal API 无法判断谁是 body因此需要把多个请求体参数封装成一个 DTO。4.2 什么时候用 Results.Ok什么时候直接返回对象直接 return 一个对象框架会把它当 200 JSON 处理。使用Results静态方法时可以精确控制状态码和响应头更符合 HTTP 语义方法HTTP 状态码场景Results.Ok(data)200查询成功Results.Created(path, data)201创建成功Results.NoContent()204更新、删除成功且不需要返回内容Results.NotFound()404资源不存在Results.BadRequest(model)400参数校验失败Results.ValidationProblem(errors)400标准模型校验错误如果接口需要返回201就应该用Created如果返回200直接返回对象也未尝不可但用Results.Ok会更语义化便于扩展响应头。4.3 用 MapGroup 管理相同前缀的接口当接口多起来后如果每个都写app.MapGet(/api/todos/...)前缀会越来越乱。Minimal API 提供MapGroup可以把同一前缀的路由集中管理var todosApi app.MapGroup(/api/todos).WithTags(Todos); todosApi.MapGet(/, () ...); todosApi.MapGet(/{id:int}, (int id) ...); todosApi.MapPost(/, (CreateTodoInput input) ...); todosApi.MapPut(/{id:int}, (int id, CreateTodoInput input) ...); todosApi.MapDelete(/{id:int}, (int id) ...);MapGroup返回的路由分组不会自动生成一个类但它可以让代码清晰很多。WithTags(Todos)的作用是在 Swagger/OpenAPI 文档里把这些接口放在同一个分组中方便查看。5. 给 Minimal API 加上 Swagger解决接口调试问题5.1 安装 Swashbuckle 包项目本身没有任何 OpenAPI 生成逻辑因为 Minimal API 不会自动继承到 Controller 的 Swagger 支持。推荐使用 Swashbuckledotnet add package Swashbuckle.AspNetCore然后修改Program.csvar builder WebApplication.CreateBuilder(args); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } // 路由和 MapGroup 保持不变 app.MapGroup(/api/todos).WithTags(Todos).MapGet(/, ...); app.Run();AddEndpointsApiExplorer负责让 Minimal API 的 endpoint 元数据暴露给 Swagger 生成器。很多新人在加了AddSwaggerGen、UseSwagger后仍然看不到接口往往就是因为漏了AddEndpointsApiExplorer。5.2 在 Swagger UI 中验证 POST 请求启动应用后访问http://localhost:5214/swagger正常情况下可以看到一个网页其中/api/todos分组下面有 GET、POST 等接口。点开 POST填入{ title: 调试 Swagger, isComplete: false }点击执行返回 201 就说明整个请求链路已经通了。生产环境是否需要开放 Swagger 要谨慎。建议只在 Development 环境启用UseSwagger和UseSwaggerUI不要直接暴露到公网。6. 把 Minimal API 发布到 IIS从 dotnet publish 到 web.config6.1 先理解 IIS 和 ASP.NET Core 的关系IIS 并不直接执行 .NET 代码。ASP.NET Core 应用默认使用 Kestrel 作为服务器IIS 只负责接收 HTTP 请求并通过 ASP.NET Core ModuleANCM转发给 Kestrel 或直接承载进程。这个过程主要有两种模式模式hostingModel工作进程方式说明In-processinprocess应用运行在w3wp.exe进程中默认模式性能更好启动更快Out-of-processoutofprocessIIS 把请求转发给独立的dotnet.exe隔离更强但性能略低Minimal API 发布后同样是 ASP.NET Core 应用IIS 部署方式和传统 Controller 项目基本一致不需要因为使用了 Minimal API 而做特殊处理。6.2 发布项目在项目目录下执行dotnet publish -c Release -o C:\publish\MinimalTodoApi发布完成后输出目录里会包含MinimalTodo.Api.dllMinimalTodo.Api.exe可选取决于运行时模式web.config一堆依赖文件和静态文件web.config是 IIS 识别应用的关键文件。如果项目发布时不生成也可以手动创建标准内容如下?xml version1.0 encodingutf-8? configuration system.webServer handlers add nameaspNetCore path* verb* modulesAspNetCoreModuleV2 resourceTypeUnspecified / /handlers aspNetCore processPathdotnet arguments.\MinimalTodo.Api.dll stdoutLogEnabledfalse stdoutLogFile.\logs\stdout hostingModelinprocess / /system.webServer /configuration如果使用框架依赖发布processPath通常是dotnetarguments指向发布目录中的 dll。如果使用自包含发布processPath可能是应用自己的.exe。6.3 在 IIS 中创建站点打开 IIS 管理器。右键“应用程序池”新建应用池.NET CLR 版本选择“无托管代码”。右键“网站”新建网站物理路径指向C:\publish\MinimalTodoApi。绑定端口比如 8080。启动网站并访问http://localhost:8080/api/todos。把应用程序池设置为“无托管代码”很重要因为 ASP.NET Core 应用不依赖传统的 .NET CLR托管代码模式反而可能导致加载错误。6.4 发布后常见状态码排查IIS 部署出现错误时页面往往只显示一个状态码不显示详细异常。以下是几个高概率问题状态码常见原因排查方向502.5进程启动失败或崩溃查看 stdout 日志和 Windows 事件查看器500.30In-process 模式启动失败查看应用代码和事件日志分析 Program.cs 初始化异常500.31找不到匹配的 .NET Runtime 版本确认服务器安装了对应 Hosting Bundle500.35同一个应用池运行多个 ASP.NET Core 应用每个应用使用独立应用池404路径、端口或根目录配置错误确认物理路径下有 dll 和 web.config排查异常时先做三件事在web.config中把stdoutLogEnabled改成true。在站点目录下建立logs文件夹并确保 IIS 工作进程有写入权限。打开 Windows 事件查看器查看“Windows 日志 - 应用程序”中来源为 ASP.NET Core Module 的日志。对于 Minimal API 来说日志中的异常多数来自Program.cs比如没有注册依赖的 Service或者数据库连接字符串在服务器上不存在。7. 常见坑与工程化最佳实践7.1 坑一把内存 List 当成生产存储很多 Minimal API 教程使用内存列表导致新手误以为 List 可以当数据库。实际上应用一重启数据就没了而且并发写入可能破坏集合。建议生产环境使用 EF Core、Dapper 或仓储访问数据库。接口中的数据库访问应该使用异步方法比如ToListAsync、SaveChangesAsync。不要在图省事的时候把可变静态集合暴露给多个请求。7.2 坑二所有接口全塞进 Program.cs“Minimal”不等于“只能在一个文件里”。项目变大后如果把几十个MapGet全写在Program.cs可读性甚至比 Controller 更差。推荐用扩展方法拆分public static class TodoEndpoints { public static void MapTodoEndpoints(this WebApplication app) { var group app.MapGroup(/api/todos).WithTags(Todos); group.MapGet(/, () Results.Ok(...)); group.MapPost(/, (CreateTodoInput input) ...); } }在Program.cs中调用
返回列表