Minimal API 是一种新的、轻量级的构建 Web API 的方法,无需额外的依赖项或样板代码。端点直接定义在 Program.cs 中,使得设置快速简单,代码量更少。
然而,有一段时间,Minimal API 缺乏对过滤器和验证的支持。
最初,如果我们想要验证请求,必须在端点主体内部手动进行,像这样:
app.MapPost("/v1/notes", (CreateNoteRequest request) =>
{
if (string.IsNullOrWhiteSpace(request.Value))
{
return Results.BadRequest();
}
if (Guid.Empty == request.Id)
{
return Results.BadRequest();
}
//将笔记存储到数据库
return Results.Ok();
});
或者,我们可以注册 FluentValidation 并使用 [FromServices] 属性从服务容器中检索验证器。代码看起来会是这样:
app.MapPost("/v2/notes", async ([FromServices] IValidator<CreateNoteRequest> validator, CreateNoteRequest request) =>
{
var validationResult = await validator.ValidateAsync(request);
if (!validationResult.IsValid)
{
return Results.BadRequest(validationResult.Errors.Select(error => error.ErrorMessage));
}
//将笔记存储到数据库
return Results.Ok();
});
但这种方法并不理想,因为它引入了几个问题。例如,它将验证与业务逻辑混在一起,这违反了单一职责原则。这也使得测试更加困难——特别是如果我们想单独测试验证逻辑和业务逻辑。
随着 .NET 7 中引入了端点过滤器,我们现在可以通过添加一个在端点被调用之前验证请求的过滤器,来轻松地将验证逻辑与业务逻辑分离。它看起来像这样:
app.MapPost("/v3/notes", (CreateNoteRequest request) =>
{
//将笔记存储到数据库
return Results.Ok();
})
.AddEndpointFilter<ValidationFilter<CreateNoteRequest>>();
public sealed class ValidationFilter<TRequest>(IValidator<TRequest> validator) : IEndpointFilter
where TRequest : class
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var argument = context.Arguments.SingleOrDefault(argument => argument?.GetType() == typeof(TRequest));
if (argument is not TRequest request)
{
throw new ArgumentException($"Could not find request with the following type `{typeof(TRequest)}`");
}
var validationResult = await validator.ValidateAsync(request);
if (!validationResult.IsValid)
{
return Results.BadRequest(validationResult.Errors.Select(error => error.ErrorMessage));
}
return await next(context);
}
}
然而,许多开发人员对这种做法并不满意,因为他们已经习惯了使用数据注解进行内置验证的模型绑定。
最终,微软在 .NET 10 中为 Minimal API 引入了这一功能。
一切旧的又是新的了
现在,您可以使用 System.ComponentModel.DataAnnotations 命名空间中的属性来验证查询字符串、标头、路由参数以及请求体。
让我们将这些属性添加到我们的模型中:
public class CreateNoteRequest
{
public Guid Id { get; set;}
[Required]
public string Value { get; set; }
}
但是,我们如何验证 Guid 以确保它不为空呢?
很简单——我将通过扩展 ValidationAttribute 来创建一个自定义验证属性。
代码如下:
[AttributeUsage(AttributeTargets.Property)]
public sealed class FilledAttribute : ValidationAttribute
{
public override bool IsValid(object? value)
{
if (value is not Guid guid)
{
return false;
}
return guid != Guid.Empty;
}
}
之后,只需将其应用于属性,像这样:[Filled] public Guid Id { get; set; }。
请注意,在预览版中,验证尚未对 Guid 生效。
但有一件重要的事情——您需要通过 DI 注册所需的服务来启用内置验证:
builder.Services.AddValidation();
您还需要在项目文件中设置 InterceptorsNamespaces 属性,像这样:
<PropertyGroup>
<InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.AspNetCore.Http.Validation.Generated</InterceptorsNamespaces>
</PropertyGroup>
是的,目前看起来有点粗糙——但它仍处于预览阶段。我猜将来它会看起来更像这样:
<PropertyGroup>
<WithValidations>true</WithValidations>
</PropertyGroup>
就这样!
现在我们的端点将看起来像这样:
app.MapPost("/v4/notes", (CreateNoteRequest request) =>
{
//将笔记存储到数据库
return Results.Ok();
});
您还可以使用 DisableValidation 扩展方法为特定端点禁用验证:
app.MapPost("/v4/notes", (CreateNoteRequest request) =>
{
//将笔记存储到数据库
return Results.Ok();
})
.DisableValidation();
一些思考
如果我们暂时搁置 Guid 验证的问题和使用 InterceptorsNamespaces 所需的笨拙设置,我们现在拥有了基于绑定模型的适当验证——就像在传统控制器中一样。