.NET 10 Minimal API 终获原生验证支持:告别手动验证,拥抱数据注解

作者:微信公众号:【架构师老卢】
9-23 14:14
157

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 所需的笨拙设置,我们现在拥有了基于绑定模型的适当验证——就像在传统控制器中一样。

相关留言评论
昵称:
邮箱:
阅读排行