构建一个经得起时间考验的.NET Web API,不仅仅是将控制器和端点连接起来。您需要一个清晰的架构、分离关注点的模式、自文档化的接口、健壮的错误处理以及开发者友好的环境。在这份增强版指南中,我们将引导您从基本概念到高级技术——包含详细的解释、最佳实践的原理以及您可以立即采用的代码片段。
当有新成员加入项目时,他们可以立即识别出在哪里找到业务规则(领域层)、工作流(应用层)和HTTP端点(表示层)。更换数据库、升级框架或重构UI代码变成了只需更改某一层而无需重写其余部分的问题。您还会对单元测试充满信心,因为您的核心逻辑不直接依赖于ASP.NET Core或Entity Framework。
层级职责
+---------------------+
| 表示层/Presentation | ←→ HTTP, 控制器, 过滤器
+---------------------+
| 应用层/Application | ←→ 用例, CQRS 接口
+---------------------+
| 领域层/Domain | ←→ 实体, 值对象, 规则
+---------------------+
| 基础设施层/Infrastructure | ←→ EF Core, 外部服务, 文件I/O
+---------------------+
src/
MyApp.Domain/
Entities/
ValueObjects/
MyApp.Application/
Interfaces/
Features/
Products/
Commands/
Queries/
DTOs/
MyApp.Infrastructure/
Data/
MyAppDbContext.cs
Repositories/
MyApp.Api/
Controllers/
Middleware/
tests/
MyApp.Application.Tests/
MyApp.Api.IntegrationTests/
Product 或 Order。示例仓储接口和DTO
// MyApp.Application/Interfaces/IProductRepository.cs
public interface IProductRepository
{
Task<IEnumerable<Product>> ListAsync(CancellationToken cancellationToken);
Task<Product> GetByIdAsync(Guid id, CancellationToken cancellationToken);
Task AddAsync(Product product, CancellationToken cancellationToken);
Task UpdateAsync(Product product, CancellationToken cancellationToken);
Task DeleteAsync(Guid id, CancellationToken cancellationToken);
}
// MyApp.Application/Features/Products/ProductDto.cs
public record ProductDto(Guid Id, string Name, decimal Price);
在此代码片段中,仓储接口定义了CRUD操作,而没有泄露EF Core的细节。ProductDto 精确地定义了API返回的数据形状,防止意外暴露内部领域状态。
MediatR 通过将命令和查询对象路由到它们各自的进程内处理程序来将它们粘合在一起。您不是直接将仓储或服务注入控制器,而是向MediatR的中介器发送请求,让它调用正确的处理程序。
设置 MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(GetAllProductsHandler).Assembly));
示例:读端(查询)
// 查询定义
public record GetAllProductsQuery() : IRequest<IEnumerable<ProductDto>>;
// 处理程序实现
public class GetAllProductsHandler : IRequestHandler<GetAllProductsQuery,
IEnumerable<ProductDto>>
{
private readonly IProductRepository _repository;
public GetAllProductsHandler(IProductRepository repository)
=> _repository = repository;
public async Task<IEnumerable<ProductDto>> Handle(GetAllProductsQuery request,
CancellationToken ct)
{
var products = await _repository.ListAsync(ct);
return products.Select(p => new ProductDto(p.Id, p.Name, p.Price));
}
}
这里,查询对象建模了一个简单的"列出所有产品"请求。它的处理程序知道如何获取领域实体,将它们映射到DTO并返回。您的控制器只需要一行代码:await _mediator.Send(new GetAllProductsQuery());
示例:写端(命令)
// 命令定义
public record CreateProductCommand(string Name, decimal Price) : IRequest<Guid>;
// 处理程序实现
public class CreateProductHandler : IRequestHandler<CreateProductCommand, Guid>
{
private readonly IProductRepository _repository;
public CreateProductHandler(IProductRepository repository)
=> _repository = repository;
public async Task<Guid> Handle(CreateProductCommand cmd, CancellationToken ct)
{
var product = new Product(cmd.Name, cmd.Price); // 强制执行领域不变性
await _repository.AddAsync(product, ct);
return product.Id;
}
}
命令代表改变状态的意图。在编写代码时,您只专注于正确构建您的领域实体。验证、日志记录或事务行为可以作为MediatR管道行为添加——保持处理程序的专注和可测试性。
当您集成 Swagger 时:
基本 Swagger 设置
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new() { Title = "MyApp API", Version = "v1" });
var xmlPath = Path.Combine(AppContext.BaseDirectory,
$"{Assembly.GetExecutingAssembly().GetName().Name}.xml");
c.IncludeXmlComments(xmlPath);
});
AddEndpointsApiExplorer() 扫描您的控制器以获取端点元数据。AddSwaggerGen() 注册Swagger生成服务,并允许您添加XML注释以获取更丰富的描述。注解您的控制器 通过使用XML注释和响应属性装饰控制器操作,您可以确保Swagger显示清晰的描述和示例模型。
/// <summary>
/// 检索目录中的所有产品。
/// </summary>
/// <returns>产品列表。</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> GetAll()
{
var dtos = await _mediator.Send(new GetAllProductsQuery());
return Ok(dtos);
}
<summary> 标签显示为方法描述。[ProducesResponseType] 阐明了可能的HTTP状态码和有效负载,改善了客户端错误处理和对SDK的自动生成。常见版本控制方法 [图示:URI路径版本控制、查询字符串版本控制、请求头版本控制]
配置版本控制
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = new UrlSegmentApiVersionReader();
});
builder.Services.AddVersionedApiExplorer(setup =>
{
setup.GroupNameFormat = "'v'VVV";
setup.SubstituteApiVersionInUrl = true;
});
DefaultApiVersion 设置当客户端未指定任何版本时的基线。ReportApiVersions 返回类似 api-supported-versions 的头部,以便客户端知道有哪些版本可用。IApiVersionDescriptionProvider 与Swagger集成,自动为每个版本生成单独的文档。按版本装饰控制器
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
// GET api/v1/products
[HttpGet]
public IActionResult GetV1() => Ok(/* v1 有效负载 */);
// GET api/v2/products
[HttpGet, MapToApiVersion("2.0")]
public IActionResult GetV2() => Ok(/* 包含额外字段的扩展 v2 有效负载 */);
}
客户端可以通过提升其URL版本段来选择加入新行为。同时,旧客户端继续访问v1而不受影响。
为什么需要全局错误处理?
中间件实现
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
public ExceptionHandlingMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (ValidationException ex)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsJsonAsync(new
{
Title = "验证失败",
Errors = ex.Errors
});
}
catch (KeyNotFoundException)
{
context.Response.StatusCode = StatusCodes.Status404NotFound;
await context.Response.WriteAsJsonAsync(new { Title = "资源未找到" });
}
catch (Exception)
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsJsonAsync(new
{
Title = "发生意外错误。",
TraceId = context.TraceIdentifier
});
}
}
}
在 Program.cs 中的路由之前注册此中间件:
app.UseMiddleware<ExceptionHandlingMiddleware>();
EditorConfig + Roslyn 分析器 强制执行一致的风格并及早捕获错误。
dotnet add package Microsoft.CodeAnalysis.FxCopAnalyzers
热重载 监视代码更改并立即应用它们,而无需重新构建。
dotnet watch --project MyApp.Api
预提交钩子
在每次提交时自动运行 dotnet format 和单元测试,以防止不良代码进入仓库。
全局工具
dotnet-ef 用于数据库迁移dotnet-format 用于代码清理dotnet-reportgenerator 用于覆盖率指标结构化日志记录和遥测 集成Serilog或Azure Application Insights。将关联ID附加到HTTP请求,以便您可以从API表面向下追踪问题直到数据库查询。
这些增强功能中的每一项都使开发人员能够充满信心和速度地编写、测试和交付功能。
通过使用清洁架构对代码进行分层、通过MediatR采用CQRS、使用Swagger进行文档记录、一致地处理版本和错误以及增强开发者工作流程,您将创建一个有弹性、可扩展的API界面。
您精通之路的下一步:
掌握了这些基础知识和详细解释后,无论您的业务需求如何发展,维护和扩展您的.NET Web API都将变得轻松愉快。