mcpField
M
minimal-apis

minimal-apis

github.com/managedcode开发49684 次安装1 天前更新
代码仓库

关于 minimal-apis

Minimal APIs 是一项开发技能,用于使用 ASP.NET Core 构建轻量 HTTP API 和微服务。内容涵盖端点组织、验证、类型化响应和 OpenAPI 文档。

主要功能

  1. 01使用 Minimal APIs 创建端点
  2. 02分组路由并共享配置
  3. 03用端点筛选器执行验证
  4. 04使用 TypedResults 提供类型化响应
  5. 05生成 OpenAPI 文档

适用场景

  1. 01构建轻量 HTTP API
  2. 02组织相关服务端点
  3. 03验证传入请求
  4. 04用 OpenAPI 编写 API 文档

安装

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

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

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

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

manifest.json
references/
anti-patterns.md
patterns.md

详细介绍

Minimal API

适用场景

  • 在 ASP.NET Core 中构建新的 HTTP API
  • 创建轻量级微服务
  • 在 Minimal API 和控制器之间做出选择
  • 使用路由组组织端点
  • 实现验证和筛选器

文档

参考资料
  • patterns.md - 详细介绍路由组、筛选器、TypedResults 模式、参数绑定、错误处理和测试
  • anti-patterns.md - 应避免的常见 Minimal API 错误

何时使用 Minimal API,何时使用控制器

使用 Minimal API使用控制器
新项目现有 MVC/API 项目
微服务复杂的模型绑定
简单的 CRUD APIOData、JsonPatch
轻量级处理程序大量使用特性
.NET 8+ 项目需要 [ApiController] 功能

工作流

  1. 直接在 Program.cs 中定义端点(适用于小型 API)
  2. 使用路由组组织相关端点
  3. 随着 API 增长,将处理程序移至单独的类
  4. 应用筛选器处理横切关注点
  5. 使用 TypedResults实现类型安全的响应
  6. 使用 .WithOpenApi() 生成 OpenAPI 文档

上游最新说明

  • dotnet/aspnetcore 的 v10.0.11 是维护版本;它不会更改 Minimal API 的路由组、筛选器或 TypedResults 模型,但会将 OpenAPI 技术栈更新到 2.7.5。升级后,请重新生成文档并检查契约快照。
  • 2026 年 8 月的 aspnetcore-10.0 概述仍将轻量级 HTTP API 指向此处。需要了解精确的 OpenAPI、筛选器或参数绑定行为时,请使用专门的 Minimal API 页面。

基本模式

简单端点
var app = builder.Build();

app.MapGet("/", () => "Hello World");

app.MapGet("/products/{id}", (int id) => Results.Ok(new { Id = id }));

app.MapPost("/products", (Product product) => Results.Created($"/products/{product.Id}", product));
TypedResults(强类型)
app.MapGet("/products/{id}", Results<Ok<Product>, NotFound> (int id, AppDb db) =>
{
    var product = db.Products.Find(id);
    return product is not null
        ? TypedResults.Ok(product)
        : TypedResults.NotFound();
});
依赖注入
app.MapGet("/products", async (IProductService service) =>
{
    return await service.GetAllAsync();
});

// Or with [FromServices] for clarity
app.MapGet("/products", async ([FromServices] IProductService service) =>
    await service.GetAllAsync());

路由组

基本分组
var products = app.MapGroup("/api/products");

products.MapGet("/", GetAll);
products.MapGet("/{id}", GetById);
products.MapPost("/", Create);
products.MapPut("/{id}", Update);
products.MapDelete("/{id}", Delete);
具有共享配置的组
var api = app.MapGroup("/api")
    .RequireAuthorization()
    .AddEndpointFilter<ValidationFilter>();

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

var orders = api.MapGroup("/orders")
    .WithTags("Orders")
    .RequireAuthorization("AdminOnly");

端点筛选器

内联筛选器
app.MapGet("/products/{id}", (int id) => Results.Ok(id))
    .AddEndpointFilter(async (context, next) =>
    {
        var id = context.GetArgument<int>(0);
        if (id <= 0)
            return Results.BadRequest("Invalid ID");

        return await next(context);
    });
基于类的筛选器
public class ValidationFilter<T> : IEndpointFilter where T : class
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var argument = context.Arguments
            .OfType<T>()
            .FirstOrDefault();

        if (argument is null)
            return Results.BadRequest("Invalid request body");

        var validator = context.HttpContext.RequestServices
            .GetService<IValidator<T>>();

        if (validator is not null)
        {
            var result = await validator.ValidateAsync(argument);
            if (!result.IsValid)
                return Results.ValidationProblem(result.ToDictionary());
        }

        return await next(context);
    }
}

// Usage
products.MapPost("/", Create)
    .AddEndpointFilter<ValidationFilter<CreateProductRequest>>();
通过根组配置全局筛选器
// All endpoints inherit filters from root group
var root = app.MapGroup("")
    .AddEndpointFilter<LoggingFilter>()
    .AddEndpointFilter<ErrorHandlingFilter>();

root.MapGet("/health", () => Results.Ok());
root.MapGroup("/api/products").MapGet("/", GetProducts);

组织较大型 API

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

        group.MapGet("/", GetAll);
        group.MapGet("/{id}", GetById);
        group.MapPost("/", Create);

        return group;
    }

    private static async Task<Ok<List<Product>>> GetAll(IProductService service)
        => TypedResults.Ok(await service.GetAllAsync());

    private static async Task<Results<Ok<Product>, NotFound>> GetById(
        int id, IProductService service)
    {
        var product = await service.GetByIdAsync(id);
        return product is not null
            ? TypedResults.Ok(product)
            : TypedResults.NotFound();
    }

    private static async Task<Created<Product>> Create(
        CreateProductRequest request, IProductService service)
    {
        var product = await service.CreateAsync(request);
        return TypedResults.Created($"/api/products/{product.Id}", product);
    }
}

// Program.cs
app.MapProductEndpoints();
app.MapOrderEndpoints();

请求/响应 DTO

// Separate from domain models
public record CreateProductRequest(string Name, decimal Price);
public record UpdateProductRequest(string Name, decimal Price);
public record ProductResponse(int Id, string Name, decimal Price);

// Don't expose domain entities directly
app.MapPost("/products", (CreateProductRequest request, IMapper mapper) =>
{
    var product = mapper.Map<Product>(request);
    // ...
    return TypedResults.Created($"/products/{product.Id}",
        mapper.Map<ProductResponse>(product));
});

应避免的反模式

反模式问题所在更好的做法
所有内容都放在 Program.cs 中难以维护使用扩展方法
不使用路由组配置重复将相关端点分组
手动验证容易出错使用筛选器 + FluentValidation
暴露实体耦合紧密使用 DTO
不使用 TypedResults缺少编译时检查使用 TypedResults
忽略 OpenAPI没有文档添加 .WithOpenApi()

OpenAPI 集成

builder.Services.AddOpenApi();

app.MapOpenApi();  // Serves OpenAPI spec

app.MapGet("/products", GetProducts)
    .WithName("GetProducts")
    .WithSummary("Get all products")
    .WithDescription("Returns a list of all available products")
    .Produces<List<Product>>(StatusCodes.Status200OK)
    .ProducesProblem(StatusCodes.Status500InternalServerError);

交付内容

  • 简洁、组织良好的 Minimal API 端点
  • 正确使用路由组和筛选器
  • 使用 TypedResults 返回类型安全的响应
  • OpenAPI 文档
  • 使用端点筛选器进行验证

验证

  • 端点返回正确的状态码
  • 验证筛选器能捕获无效输入
  • OpenAPI 规范准确无误
  • 路由组共享通用配置
  • 处理程序可测试(可以模拟依赖项)

评论

支持 Markdown