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