dotnet-web-api

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ASP.NET Core Web API

ASP.NET Core Web API

Trigger On

触发场景

  • working on controller-based APIs in ASP.NET Core
  • needing controller-specific extensibility or conventions
  • migrating or reviewing existing API controllers and filters
  • 开发ASP.NET Core中基于控制器的API时
  • 需要控制器特定的扩展能力或约定时
  • 迁移或审查现有API控制器和过滤器时

Workflow

工作流程

  1. Use controllers when the API needs controller-centric features, not simply because older templates did so.
  2. Keep controllers thin: map HTTP concerns to application services or handlers, and avoid embedding data access and business rules directly in actions.
  3. Use clear DTO boundaries, explicit validation, and predictable HTTP status behavior.
  4. Review authentication and authorization at both controller and endpoint levels so the API surface is not accidentally inconsistent.
  5. Keep OpenAPI generation, versioning, and error contract behavior deliberate rather than incidental.
  6. Use
    dotnet-minimal-apis
    for new simple APIs instead of defaulting to controllers out of habit.
  1. 只有当API需要以控制器为中心的特性时才使用控制器,不要仅仅因为旧模板这么做就沿用。
  2. 保持控制器轻量化:将HTTP相关逻辑映射到应用服务或处理程序,避免在动作方法中直接嵌入数据访问和业务规则。
  3. 使用清晰的DTO边界、显式验证和可预测的HTTP状态码行为。
  4. 在控制器和端点两个层面审查身份认证和授权逻辑,避免API接口权限出现意外不一致。
  5. 刻意设计OpenAPI生成、版本控制和错误契约行为,不要让其成为默认附带的偶然产物。
  6. 对于新增的简单API,使用
    dotnet-minimal-apis
    ,不要出于习惯默认选择控制器。

Deliver

交付结果

  • controller APIs with explicit contracts and policies
  • reduced controller bloat
  • tests or smoke checks for critical API behavior
  • 具备显式契约和策略的控制器API
  • 减少控制器代码臃肿
  • 针对关键API行为的测试或冒烟检查

Validate

验证标准

  • controller features are actually justified
  • actions do not hide business logic and persistence details
  • HTTP semantics stay predictable across endpoints
  • 控制器特性的使用确实有合理依据
  • 动作方法没有隐藏业务逻辑和持久化细节
  • 所有端点的HTTP语义保持可预测

Controller Structure

控制器结构

Use primary constructors (C# 12+) for dependency injection and keep controllers focused on HTTP concerns:
csharp
[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);
    }
}
使用主构造函数(C# 12及以上版本)实现依赖注入,让控制器只聚焦于HTTP相关逻辑:
csharp
[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);
    }
}

Model Binding

模型绑定

Explicitly declare binding sources for clarity:
csharp
[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
}
Use record types with required members for request DTOs:
csharp
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; } = [];
}
显式声明绑定源以提升可读性:
csharp
[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类型:
csharp
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; } = [];
}

Validation

验证

Prefer FluentValidation for complex validation rules:
csharp
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);
        });
    }
}
Configure consistent Problem Details responses:
csharp
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);
    };
});
复杂验证规则优先使用FluentValidation:
csharp
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);
        });
    }
}
配置统一的问题详情响应:
csharp
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 Versioning

API版本控制

Configure URL path versioning:
csharp
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);
    }
}
配置URL路径版本控制:
csharp
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);
    }
}

Exception Handling

异常处理

Use global exception handlers for consistent error responses:
csharp
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;
    }
}
使用全局异常处理器实现统一的错误响应:
csharp
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;
    }
}

References

参考资料

  • patterns.md - Controller patterns, model binding, validation, versioning, response handling, and filter patterns
  • anti-patterns.md - Common API mistakes to avoid including fat controllers, inconsistent errors, missing cancellation tokens, and improper HTTP semantics
  • patterns.md - 控制器模式、模型绑定、验证、版本控制、响应处理和过滤器模式
  • anti-patterns.md - 需要避免的常见API错误,包括臃肿控制器、不一致错误响应、缺少取消令牌和不当HTTP语义