mcpField
M
minimal-api

minimal-api

github.com/codewithmukesh开发7913.0k 次安装12 周前更新
代码仓库

关于 minimal-api

面向 .NET 10 开发者的 Minimal APIs 指南,介绍端点组织、类型安全响应及 API 文档实践。通过端点组自动发现,避免在 Program.cs 中逐项注册端点。

主要功能

  1. 01端点组自动发现
  2. 02TypedResults 类型安全响应
  3. 03参数绑定模式
  4. 04端点筛选器
  5. 05内置 OpenAPI 配置
  6. 06限流与输出缓存

适用场景

  1. 01构建新的 HTTP API
  2. 02按功能组织 API 端点
  3. 03生成带类型信息的 OpenAPI 文档
  4. 04为端点添加验证或限流

安装

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

$npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill minimal-api

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

详细介绍

Minimal API(.NET 10)

核心原则

  1. 默认使用 Minimal API — 只有在迁移旧代码时才使用控制器。Minimal API 更轻量、速度更快,并且能与任何架构风格良好组合。
  2. 使用 MapGroup 对端点分组 — 不要在 Program.cs 中零散地调用 MapGet/MapPost。将相关端点归为一组。
  3. 使用 TypedResults 支持 OpenAPI — TypedResults.Ok(value) 提供编译时类型安全,并生成正确的 OpenAPI 文档。Results.Ok(value) 则不会。
  4. 使用元数据,而不是注释 — 使用 .WithName()、.WithTags()、.WithSummary() 为端点添加文档说明。元数据会写入 OpenAPI 规范。

模式

端点组自动发现(必需模式)

每个端点组都放在单独的文件中,并实现 IEndpointGroup。在 Program.cs 中调用一次 app.MapEndpoints(),即可自动发现并注册所有组。添加新的端点组时,无需修改 Program.cs。

// Extensions/IEndpointGroup.cs
public interface IEndpointGroup
{
    void Map(IEndpointRouteBuilder app);
}
// Extensions/EndpointExtensions.cs
public static class EndpointExtensions
{
    public static WebApplication MapEndpoints(this WebApplication app)
    {
        var groups = typeof(Program).Assembly
            .GetTypes()
            .Where(t => t.IsAssignableTo(typeof(IEndpointGroup)) && !t.IsInterface && !t.IsAbstract)
            .Select(Activator.CreateInstance)
            .Cast<IEndpointGroup>();

        foreach (var group in groups)
            group.Map(app);

        return app;
    }
}
// Program.cs — this NEVER changes when adding endpoints
var app = builder.Build();
app.MapEndpoints();
app.Run();
// Features/Orders/OrderEndpoints.cs — one file per endpoint group
public sealed class OrderEndpoints : IEndpointGroup
{
    public void Map(IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/orders").WithTags("Orders");

        group.MapPost("/", CreateOrder)
            .WithName("CreateOrder")
            .WithSummary("Create a new order")
            .Produces<OrderResponse>(StatusCodes.Status201Created)
            .ProducesValidationProblem()
            .RequireAuthorization();

        group.MapGet("/{id:guid}", GetOrder)
            .WithName("GetOrder")
            .Produces<OrderResponse>()
            .ProducesProblem(StatusCodes.Status404NotFound);

        group.MapGet("/", ListOrders)
            .WithName("ListOrders")
            .Produces<PagedList<OrderResponse>>();
    }

    private static async Task<Results<Created<OrderResponse>, ValidationProblem>> CreateOrder(
        CreateOrderRequest request,
        ISender sender,
        CancellationToken ct)
    {
        var result = await sender.Send(new CreateOrder.Command(request.CustomerId, request.Items), ct);
        return result.IsSuccess
            ? TypedResults.Created($"/api/orders/{result.Value.Id}", result.Value)
            : TypedResults.ValidationProblem(result.Errors);
    }

    private static async Task<Results<Ok<OrderResponse>, NotFound>> GetOrder(
        Guid id,
        ISender sender,
        CancellationToken ct)
    {
        var result = await sender.Send(new GetOrder.Query(id), ct);
        return result.IsSuccess
            ? TypedResults.Ok(result.Value)
            : TypedResults.NotFound();
    }

    private static async Task<Ok<PagedList<OrderResponse>>> ListOrders(
        [AsParameters] ListOrdersQuery query,
        ISender sender,
        CancellationToken ct)
    {
        var result = await sender.Send(query, ct);
        return TypedResults.Ok(result);
    }
}
使用 TypedResults 实现类型安全的响应

TypedResults 提供编译时保证,并自动生成 OpenAPI 架构。

// GOOD — TypedResults with union return type
private static async Task<Results<Ok<Product>, NotFound, ValidationProblem>> GetProduct(
    Guid id,
    AppDbContext db,
    CancellationToken ct)
{
    var product = await db.Products.FindAsync([id], ct);
    return product is not null
        ? TypedResults.Ok(product)
        : TypedResults.NotFound();
}
参数绑定

.NET 10 Minimal API 会自动从路由、查询字符串、标头、请求正文和 DI 绑定参数。

// Route parameters
app.MapGet("/orders/{id:guid}", (Guid id) => ...);

// Query parameters (nullable = optional)
app.MapGet("/orders", (int page, int? pageSize, string? status) => ...);

// Complex query parameters with [AsParameters]
public record ListOrdersQuery(int Page = 1, int PageSize = 20, string? Status = null);
app.MapGet("/orders", ([AsParameters] ListOrdersQuery query) => ...);

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

// DI services are auto-resolved (no attribute needed)
app.MapPost("/orders", (CreateOrderRequest request, ISender sender) => ...);
端点筛选器

筛选器相当于 Minimal API 中的操作筛选器。可将其用于验证、日志记录和幂等性检查等横切关注点。

标准的 ValidationFilter<TRequest> 实现(使用 FluentValidation、从 DI 解析验证器,并在未注册验证器时正常跳过)位于 error-handling 技能中 — 请使用该实现,不要在各个项目中重复实现。

// Apply the canonical filter (see error-handling skill) to a mutating endpoint
group.MapPost("/", CreateOrder)
    .AddEndpointFilter<ValidationFilter<CreateOrderRequest>>();

// Apply a filter to a group (affects all endpoints in the group)
group.AddEndpointFilter<LoggingFilter>();
OpenAPI / Swagger 配置

.NET 10 内置了 OpenAPI 支持。请使用内置支持,而不是 Swashbuckle。

// Program.cs — service registration only, no endpoint wiring
builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}
app.MapEndpoints(); // auto-discovers all IEndpointGroup implementations

// Endpoint metadata enriches the OpenAPI spec
group.MapPost("/", CreateOrder)
    .WithName("CreateOrder")
    .WithSummary("Create a new order")
    .WithDescription("Creates a new order for the specified customer with the given line items.")
    .Produces<OrderResponse>(StatusCodes.Status201Created)
    .ProducesValidationProblem()
    .ProducesProblem(StatusCodes.Status500InternalServerError);
速率限制
builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("api", opt =>
    {
        opt.PermitLimit = 100;
        opt.Window = TimeSpan.FromMinutes(1);
    });
});

// Apply inside an IEndpointGroup.Map method
var group = app.MapGroup("/api/orders")
    .WithTags("Orders")
    .RequireRateLimiting("api");
输出缓存
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder.Expire(TimeSpan.FromMinutes(5)));
    options.AddPolicy("ByIdCache", builder => builder
        .Expire(TimeSpan.FromMinutes(10))
        .SetVaryByRouteValue("id"));
});

group.MapGet("/{id:guid}", GetOrder)
    .CacheOutput("ByIdCache");

反模式

不要将端点放在 Program.cs 中
// BAD — endpoints scattered in Program.cs
app.MapGet("/orders", async (AppDbContext db) => await db.Orders.ToListAsync());
app.MapGet("/orders/{id}", async (Guid id, AppDbContext db) => await db.Orders.FindAsync(id));
app.MapPost("/orders", async (Order order, AppDbContext db) => { /* ... */ });
app.MapGet("/products", async (AppDbContext db) => await db.Products.ToListAsync());

// ALSO BAD — manual MapGroup calls in Program.cs (grows with every feature)
app.MapGroup("/api/orders").WithTags("Orders").MapOrderEndpoints();
app.MapGroup("/api/products").WithTags("Products").MapProductEndpoints();
app.MapGroup("/api/customers").WithTags("Customers").MapCustomerEndpoints();
// Program.cs grows every time you add a feature...

// GOOD — auto-discovered, Program.cs never changes
app.MapEndpoints(); // discovers all IEndpointGroup implementations
不要使用无类型结果
// BAD — Results.Ok doesn't contribute to OpenAPI schema
private static async Task<IResult> GetOrder(Guid id, AppDbContext db)
{
    var order = await db.Orders.FindAsync(id);
    return order is not null ? Results.Ok(order) : Results.NotFound();
}

// GOOD — TypedResults with explicit union type
private static async Task<Results<Ok<Order>, NotFound>> GetOrder(Guid id, AppDbContext db)
{
    var order = await db.Orders.FindAsync(id);
    return order is not null ? TypedResults.Ok(order) : TypedResults.NotFound();
}
不要直接返回领域实体
// BAD — leaks internal structure, can't evolve independently
app.MapGet("/orders/{id}", async (Guid id, AppDbContext db) =>
    await db.Orders.Include(o => o.Items).FirstOrDefaultAsync(o => o.Id == id));

// GOOD — map to a response DTO
app.MapGet("/orders/{id}", async (Guid id, AppDbContext db) =>
{
    var order = await db.Orders
        .Where(o => o.Id == id)
        .Select(o => new OrderResponse(o.Id, o.Total, o.CreatedAt))
        .FirstOrDefaultAsync();
    return order is not null ? TypedResults.Ok(order) : TypedResults.NotFound();
});

决策指南

场景建议
新建 HTTP API每个功能使用一个 IEndpointGroup,并通过 app.MapEndpoints() 自动发现
现有 MVC 项目保留控制器,逐步迁移
OpenAPI 文档使用 TypedResults + .WithName() + .WithSummary()
请求验证使用基于 FluentValidation 的端点筛选器
身份验证/授权在组或端点上使用 .RequireAuthorization("PolicyName")
速率限制使用 AddRateLimiter + .RequireRateLimiting()
响应缓存使用 AddOutputCache + .CacheOutput()
复杂模型绑定使用带有记录类型的 [AsParameters]

评论

支持 Markdown