mcpField
D
dotnet-minimal-apis

dotnet-minimal-apis

github.com/wshaddix开发8694 次安装35 周前更新
代码仓库

关于 dotnet-minimal-apis

面向 .NET 开发者的 ASP.NET Core Minimal API 实践技能,涵盖路由分组、筛选器、类型化响应、参数绑定与 OpenAPI 指南。

主要功能

  1. 01路由组与嵌套路由
  2. 02端点筛选器管线
  3. 03TypedResults 与响应架构
  4. 04OpenAPI 3.1 集成
  5. 05参数绑定与 JSON 配置
  6. 06端点组织模式

适用场景

  1. 01组织相关 API 端点
  2. 02应用共享的验证或日志筛选器
  3. 03为 OpenAPI 生成响应架构
  4. 04规划不断扩大的 Minimal API 项目

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-minimal-apis

安装后在 Claude Code 中输入/dotnet-minimal-apis确认已加载。

详细介绍

dotnet-minimal-apis

Minimal API 是 Microsoft 针对新的 ASP.NET Core HTTP API 项目推荐的方案。它们提供轻量、基于 lambda 的编程模型,原生支持 OpenAPI、用于处理横切关注点的端点筛选器,以及便于大规模组织端点的路由组。

不在本文范围内: API 版本控制策略——请参阅 [skill:dotnet-api-versioning]。输入验证框架和模式——请参阅 [skill:dotnet-input-validation]。架构模式(垂直切片、CQRS、整洁架构)——请参阅 [skill:dotnet-architecture-patterns]。身份验证和授权实现——请参阅 [skill:dotnet-api-security]。OpenAPI 文档生成和自定义——请参阅 [skill:dotnet-openapi]。

交叉引用:[skill:dotnet-architecture-patterns] 介绍大型 API 的组织方式,[skill:dotnet-input-validation] 介绍请求验证,[skill:dotnet-api-versioning] 介绍版本控制策略,[skill:dotnet-openapi] 介绍 OpenAPI 自定义。


路由组

路由组可将相关端点组织在共享前缀下,并一次性应用通用配置(筛选器、元数据、授权)。它们通过共享前缀取代重复的 MapGet/MapPost 链式调用。

var app = builder.Build();

// Group endpoints under /api/products with shared configuration
var products = app.MapGroup("/api/products")
    .WithTags("Products")
    .RequireAuthorization();

products.MapGet("/", async (AppDbContext db) =>
    TypedResults.Ok(await db.Products.ToListAsync()));

products.MapGet("/{id:int}", async (int id, AppDbContext db) =>
    await db.Products.FindAsync(id) is Product product
        ? TypedResults.Ok(product)
        : TypedResults.NotFound());

products.MapPost("/", async (CreateProductDto dto, AppDbContext db) =>
{
    var product = new Product { Name = dto.Name, Price = dto.Price };
    db.Products.Add(product);
    await db.SaveChangesAsync();
    return TypedResults.Created($"/api/products/{product.Id}", product);
});

products.MapDelete("/{id:int}", async (int id, AppDbContext db) =>
{
    if (await db.Products.FindAsync(id) is not Product product)
        return TypedResults.NotFound();

    db.Products.Remove(product);
    await db.SaveChangesAsync();
    return TypedResults.NoContent();
});
嵌套路由组

可以嵌套路由组,以组合前缀和筛选器:

var api = app.MapGroup("/api")
    .AddEndpointFilter<RequestLoggingFilter>();

var v1 = api.MapGroup("/v1");
var products = v1.MapGroup("/products").WithTags("Products");
var orders = v1.MapGroup("/orders").WithTags("Orders");

// Registers as: GET /api/v1/products
products.MapGet("/", GetProducts);
// Registers as: POST /api/v1/orders
orders.MapPost("/", CreateOrder);

端点筛选器

端点筛选器提供了一个处理横切关注点(日志记录、验证、授权增强)的管道,类似于 MVC 操作筛选器,但专用于 Minimal API。

IEndpointFilter 接口
public sealed class ValidationFilter<T>(IValidator<T> validator) : IEndpointFilter
    where T : class
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        // Extract the argument of type T from the endpoint parameters
        var argument = context.Arguments
            .OfType<T>()
            .FirstOrDefault();

        if (argument is null)
            return TypedResults.BadRequest("Request body is required");

        var result = await validator.ValidateAsync(argument);
        if (!result.IsValid)
        {
            return TypedResults.ValidationProblem(
                result.ToDictionary());
        }

        return await next(context);
    }
}
应用筛选器
// Apply to a single endpoint
products.MapPost("/", CreateProduct)
    .AddEndpointFilter<ValidationFilter<CreateProductDto>>();

// Apply to an entire route group
var products = app.MapGroup("/api/products")
    .AddEndpointFilter<RequestLoggingFilter>();

// Inline filter using a lambda
products.MapGet("/{id:int}", GetProductById)
    .AddEndpointFilter(async (context, next) =>
    {
        var id = context.GetArgument<int>(0);
        if (id <= 0)
            return TypedResults.BadRequest("ID must be positive");

        return await next(context);
    });
筛选器执行顺序

筛选器按注册顺序执行(最先注册的位于最外层)。所有筛选器均通过后,才会运行端点处理程序:

Request -> Filter1 -> Filter2 -> Filter3 -> Handler
Response <- Filter1 <- Filter2 <- Filter3 <-

TypedResults

Minimal API 的返回值始终使用 TypedResults(静态工厂),而不是 Results(接口工厂)。TypedResults 会返回具体类型,OpenAPI 元数据生成器可以在构建时检查这些类型,从而自动生成准确的响应架构。

// PREFERRED: TypedResults -- concrete return types, auto-generates OpenAPI metadata
products.MapGet("/{id:int}", async Task<Results<Ok<Product>, NotFound>> (
    int id, AppDbContext db) =>
    await db.Products.FindAsync(id) is Product product
        ? TypedResults.Ok(product)
        : TypedResults.NotFound());

// AVOID: Results -- returns IResult, OpenAPI generator cannot infer response types
products.MapGet("/{id:int}", async (int id, AppDbContext db) =>
    await db.Products.FindAsync(id) is Product product
        ? Results.Ok(product)
        : Results.NotFound());
联合返回类型

使用 Results<T1, T2, ...> 声明单个端点的所有可能响应类型。这样可以生成包含多个响应代码的准确 OpenAPI 文档:

products.MapPost("/", async Task<Results<Created<Product>, ValidationProblem, Conflict>> (
    CreateProductDto dto, AppDbContext db) =>
{
    if (await db.Products.AnyAsync(p => p.Sku == dto.Sku))
        return TypedResults.Conflict();

    var product = new Product { Name = dto.Name, Sku = dto.Sku, Price = dto.Price };
    db.Products.Add(product);
    await db.SaveChangesAsync();
    return TypedResults.Created($"/api/products/{product.Id}", product);
});

OpenAPI 3.1 集成

.NET 10 通过 Microsoft.AspNetCore.OpenApi 增加了内置的 OpenAPI 3.1 支持。Minimal API 会根据 TypedResults、参数绑定和特性自动生成 OpenAPI 元数据。

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi(); // Serves /openapi/v1.json
}
丰富元数据
products.MapGet("/{id:int}", GetProductById)
    .WithName("GetProductById")
    .WithSummary("Get a product by its ID")
    .WithDescription("Returns the product details for the specified ID, or 404 if not found.")
    .Produces<Product>(StatusCodes.Status200OK)
    .ProducesProblem(StatusCodes.Status404NotFound);

关于高级 OpenAPI 自定义(文档转换器、操作转换器、架构自定义),请参阅 [skill:dotnet-openapi]。


大规模组织模式

当 API 的端点数量超过少数几个时,应将端点组织到单独的静态类或扩展方法中。

扩展方法模式
// ProductEndpoints.cs
public static class ProductEndpoints
{
    public static RouteGroupBuilder MapProductEndpoints(this IEndpointRouteBuilder routes)
    {
        var group = routes.MapGroup("/api/products")
            .WithTags("Products");

        group.MapGet("/", GetAll);
        group.MapGet("/{id:int}", GetById);
        group.MapPost("/", Create);
        group.MapPut("/{id:int}", Update);
        group.MapDelete("/{id:int}", Delete);

        return group;
    }

    private static async Task<Ok<List<Product>>> GetAll(AppDbContext db) =>
        TypedResults.Ok(await db.Products.ToListAsync());

    private static async Task<Results<Ok<Product>, NotFound>> GetById(
        int id, AppDbContext db) =>
        await db.Products.FindAsync(id) is Product p
            ? TypedResults.Ok(p)
            : TypedResults.NotFound();

    private static async Task<Created<Product>> Create(
        CreateProductDto dto, AppDbContext db)
    {
        var product = new Product { Name = dto.Name, Price = dto.Price };
        db.Products.Add(product);
        await db.SaveChangesAsync();
        return TypedResults.Created($"/api/products/{product.Id}", product);
    }

    private static async Task<Results<NoContent, NotFound>> Update(
        int id, UpdateProductDto dto, AppDbContext db)
    {
        var product = await db.Products.FindAsync(id);
        if (product is null) return TypedResults.NotFound();

        product.Name = dto.Name;
        product.Price = dto.Price;
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    private static async Task<Results<NoContent, NotFound>> Delete(
        int id, AppDbContext db)
    {
        var product = await db.Products.FindAsync(id);
        if (product is null) return TypedResults.NotFound();

        db.Products.Remove(product);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }
}

// Program.cs
app.MapProductEndpoints();
app.MapOrderEndpoints();
app.MapCustomerEndpoints();
Carter 库

对于倾向于自动发现端点模块的项目,Carter 库提供了 ICarterModule 接口:

// <PackageReference Include="Carter" Version="8.*" />
public sealed class ProductModule : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/products").WithTags("Products");

        group.MapGet("/", async (AppDbContext db) =>
            TypedResults.Ok(await db.Products.ToListAsync()));

        group.MapGet("/{id:int}", async (int id, AppDbContext db) =>
            await db.Products.FindAsync(id) is Product p
                ? TypedResults.Ok(p)
                : TypedResults.NotFound());
    }
}

// Program.cs
builder.Services.AddCarter();
var app = builder.Build();
app.MapCarter(); // Auto-discovers and registers all ICarterModule implementations
垂直切片组织

对于采用垂直切片架构的项目(请参阅 [skill:dotnet-architecture-patterns]),每个功能都在单个目录中管理自己的端点、处理程序和模型:

Features/
  Products/
    GetProducts.cs       # Endpoint + handler + response DTO
    CreateProduct.cs     # Endpoint + handler + request/response DTOs
    UpdateProduct.cs
    DeleteProduct.cs
    ProductEndpoints.cs  # Route group registration

JSON 配置

Minimal API 默认使用 System.Text.Json。可以全局配置所有 Minimal API 端点的 JSON 选项:

// ConfigureHttpJsonOptions applies to Minimal APIs ONLY, not MVC controllers
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
    options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
    options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});

注意: ConfigureHttpJsonOptions 仅配置 Minimal API 的 JSON 序列化。MVC 控制器使用单独的管道——通过 builder.Services.AddControllers().AddJsonOptions(...) 配置。混淆两者不会产生任何效果。


参数绑定

Minimal API 会根据类型和特性注解,自动从路由、查询、标头、请求正文和 DI 中绑定参数。

// Route parameter (from URL segment)
app.MapGet("/products/{id:int}", (int id) => ...);

// Query string
app.MapGet("/products", ([FromQuery] int page, [FromQuery] int pageSize) => ...);

// Header
app.MapGet("/products", ([FromHeader(Name = "X-Correlation-Id")] string correlationId) => ...);

// Body (JSON deserialized)
app.MapPost("/products", (CreateProductDto dto) => ...);

// DI-injected services (resolved automatically)
app.MapGet("/products", (AppDbContext db, ILogger<Program> logger) => ...);

// AsParameters: bind a complex object from multiple sources
app.MapGet("/products", ([AsParameters] ProductQuery query) => ...);

public record ProductQuery(
    [FromQuery] int Page = 1,
    [FromQuery] int PageSize = 20,
    [FromQuery] string? SortBy = null);

Agent 注意事项

  1. 如果可以使用 TypedResults,就不要使用 Results——Results.Ok(value) 返回 IResult,OpenAPI 生成器无法推断响应架构。使用 TypedResults.Ok(value) 可启用自动架构生成。
  2. 不要忘记 ConfigureHttpJsonOptions 仅适用于 Minimal API——MVC 控制器需要单独调用 .AddControllers().AddJsonOptions()。
  3. 不要在每个端点中内联应用验证逻辑——请使用端点筛选器,或参阅 [skill:dotnet-input-validation],了解集中式验证模式。
  4. 不要以错误的顺序注册筛选器——先注册的筛选器位于最外层。先放置范围较广的筛选器(日志记录),再放置更具体的筛选器(验证),使其更接近处理程序。
  5. 不要将所有端点都放在 Program.cs 中——端点数量超过几个后,请整理到扩展方法类或 Carter 模块中。

前提条件

  • .NET 8.0+(Minimal API 使用路由组和端点筛选器的 LTS 基线版本)
  • .NET 10.0,支持内置 OpenAPI 3.1、SSE 和内置验证
  • 用于生成 OpenAPI 文档的 Microsoft.AspNetCore.OpenApi
  • 用于自动发现端点模块的 Carter(可选)

知识来源

本技能中的 Minimal API 模式以以下指南为依据:

这些来源为上述模式和原理提供参考。本技能并不声称代表或替任何个人发言。


参考资料

评论

支持 Markdown