mcpField
W
web-api

web-api

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

关于 web-api

用于设计、审查和维护基于控制器的 ASP.NET Core API 的技能。它强调明确的 HTTP 契约与策略,并帮助团队判断何时应使用控制器而非 Minimal APIs。

主要功能

  1. 01控制器结构与精简操作
  2. 02明确模型绑定和 DTO 边界
  3. 03验证与一致的错误响应
  4. 04API 版本控制与异常处理

适用场景

  1. 01审查现有 API 控制器和筛选器
  2. 02构建 HTTP 行为可预测的控制器 API
  3. 03减少控制器中的业务逻辑和持久化代码

安装

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

$npx skills add https://github.com/managedcode/dotnet-skills --skill web-api

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

包含的文件

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

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

详细介绍

ASP.NET Core Web API

触发条件

  • 在 ASP.NET Core 中开发基于控制器的 API
  • 需要控制器专属的扩展能力或约定
  • 迁移或审查现有 API 控制器和筛选器

工作流

  1. 只有在 API 需要以控制器为中心的功能时才使用控制器,不要仅仅因为旧模板如此就采用控制器。
  2. 保持控制器精简:将 HTTP 相关职责交给应用服务或处理程序,避免在操作方法中直接嵌入数据访问和业务规则。
  3. 明确 DTO 边界,显式进行验证,并确保 HTTP 状态行为可预测。
  4. 在控制器和端点两个层面检查身份验证与授权,避免 API 表面出现意外的不一致。
  5. 有意识地处理 OpenAPI 生成、版本控制和错误契约行为,而不是任其偶然形成。
  6. 对于新的简单 API,使用 minimal-apis,而不是习惯性地默认使用控制器。

当前上游说明

  • dotnet/aspnetcore v10.0.11 属于维护版本,并将 OpenAPI 堆栈更新至 2.7.5;基于控制器的 API 指南仍取决于约定、高级模型绑定、OData、JsonPatch 和现有筛选器。升级后,重新运行生成文档和客户端契约检查。
  • 2026 年 8 月的 aspnetcore-10.0 概述仍将控制器 API 与 Minimal APIs 并列,而非用后者取代前者。更改公共 API 契约前,请参考专门的路由、OpenAPI、身份验证和托管页面。

交付内容

  • 具有明确契约和策略的控制器 API
  • 精简的控制器
  • 针对关键 API 行为的测试或冒烟检查

验证

  • 确实有理由使用控制器功能
  • 操作方法不会隐藏业务逻辑和持久化细节
  • 各端点的 HTTP 语义保持可预测

控制器结构

使用主构造函数(C# 12+)进行依赖注入,并让控制器专注于 HTTP 相关职责:

[ApiController]
[Route("api/[controller]")]
public class OrdersController(
    IOrderService orderService,
    ILogger<OrdersController> logger) : ControllerBase
{
    [HttpGet("{id:guid}")]
    [ProducesResponseType<OrderDto>(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetById(Guid id, CancellationToken ct)
    {
        var order = await orderService.GetByIdAsync(id, ct);
        return order is null ? NotFound() : Ok(order);
    }

    [HttpPost]
    [ProducesResponseType<OrderDto>(StatusCodes.Status201Created)]
    [ProducesResponseType<ValidationProblemDetails>(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> Create(CreateOrderRequest request, CancellationToken ct)
    {
        var order = await orderService.CreateAsync(request, ct);
        return CreatedAtAction(nameof(GetById), new { id = order.Id }, order);
    }
}

模型绑定

显式声明绑定来源以提高清晰度:

[HttpGet("{id:guid}")]
public async Task<IActionResult> GetWithOptions(
    [FromRoute] Guid id,
    [FromQuery] bool includeDeleted = false,
    [FromHeader(Name = "X-Correlation-Id")] string? correlationId = null,
    CancellationToken ct = default)
{
    // Route: id, Query: includeDeleted, Header: X-Correlation-Id
}

为请求 DTO 使用带有必需成员的 record 类型:

public record CreateProductRequest
{
    public required string Name { get; init; }
    public required decimal Price { get; init; }
    public string? Description { get; init; }
    public IReadOnlyList<string> Tags { get; init; } = [];
}

验证

对于复杂的验证规则,优先使用 FluentValidation:

public class CreateOrderRequestValidator : AbstractValidator<CreateOrderRequest>
{
    public CreateOrderRequestValidator(IProductRepository products)
    {
        RuleFor(x => x.CustomerId)
            .NotEmpty()
            .WithMessage("Customer ID is required");

        RuleFor(x => x.Items)
            .NotEmpty()
            .WithMessage("Order must contain at least one item");

        RuleForEach(x => x.Items).ChildRules(item =>
        {
            item.RuleFor(i => i.ProductId)
                .NotEmpty()
                .MustAsync(async (id, ct) => await products.ExistsAsync(id, ct))
                .WithMessage("Product does not exist");

            item.RuleFor(i => i.Quantity)
                .GreaterThan(0)
                .LessThanOrEqualTo(100);
        });
    }
}

配置一致的 Problem Details 响应:

builder.Services.Configure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = context =>
    {
        var problemDetails = new ValidationProblemDetails(context.ModelState)
        {
            Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
            Title = "One or more validation errors occurred.",
            Status = StatusCodes.Status400BadRequest,
            Instance = context.HttpContext.Request.Path
        };

        return new BadRequestObjectResult(problemDetails);
    };
});

API 版本控制

配置 URL 路径版本控制:

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = new UrlSegmentApiVersionReader();
})
.AddApiExplorer(options =>
{
    options.GroupNameFormat = "'v'VVV";
    options.SubstituteApiVersionInUrl = true;
});

[ApiController]
[Route("api/v{version:apiVersion}/products")]
[ApiVersion("1.0")]
public class ProductsV1Controller(IProductService productService) : ControllerBase
{
    [HttpGet("{id}")]
    public async Task<IActionResult> Get(int id, CancellationToken ct)
    {
        var product = await productService.GetAsync(id, ct);
        return Ok(product);
    }
}

异常处理

使用全局异常处理程序,确保错误响应一致:

public class GlobalExceptionHandler(
    ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        logger.LogError(exception, "Unhandled exception occurred");

        var problemDetails = exception switch
        {
            ValidationException validationEx => new ProblemDetails
            {
                Status = StatusCodes.Status400BadRequest,
                Title = "Validation Error",
                Detail = validationEx.Message
            },
            NotFoundException notFoundEx => new ProblemDetails
            {
                Status = StatusCodes.Status404NotFound,
                Title = "Resource Not Found",
                Detail = notFoundEx.Message
            },
            _ => new ProblemDetails
            {
                Status = StatusCodes.Status500InternalServerError,
                Title = "Internal Server Error"
            }
        };

        problemDetails.Extensions["traceId"] = httpContext.TraceIdentifier;

        httpContext.Response.StatusCode = problemDetails.Status ?? 500;
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);

        return true;
    }
}

参考资料

  • patterns.md - 控制器模式、模型绑定、验证、版本控制、响应处理和筛选器模式
  • anti-patterns.md - 应避免的常见 API 错误,包括控制器臃肿、错误不一致、缺少取消令牌和 HTTP 语义不当

评论

支持 Markdown