乐趣区

关于c#:如何创建一个验证请求的API框架

​开发一款胜利软件的要害是良好的架构设计。优良的设计不仅容许开发人员轻松地编写新性能,而且还能丝滑的适应各种变动。

好的设计应该关注应用程序的外围,即畛域。

可怜的是,这很容易将畛域与不属于这一层的职责混同。每减少一个性能,就会使了解外围畛域变得更加艰难。同样蹩脚的是,未来就更难重构了。

因而,爱护畛域层不受利用程序逻辑影响是很重要的。其中一个优化是对传入申请的验证。为了避免验证逻辑渗透到畛域级别,咱们心愿在申请达到畛域级别之前验证申请。

在这篇文章中,咱们将学习如何从畛域层中提取验证。在咱们开始之前,本文假如 API 应用 command 模式将传入申请转换为命令或查问。本文中所有的代码片段都应用了 MediatR。

command 模式的益处是将外围逻辑从 API 层分离出来。大多数实现 command 模式的库也公开了能够连贯到其中的中间件。这很有用,因为它提供了一个解决方案,能够增加须要与每个命令一起执行的利用程序逻辑。

MediatR 申请

应用 C# 9 中引入的 record 类型,它能够把申请变成一行代码。另一个益处是,实例是不可变的,这使得所有变得可预测和牢靠。

record AddProductToCartCommand(Guid CartId, string Sku, int Amount) : MediatR.IRequest;

为了散发上述命令,能够将传入的申请映射到控制器中。

[ApiController]
[Route("[controller]")]
public class CustomerCartsController : ControllerBase
{
    private readonly IMediator _mediator;
​
    public CustomerCartsController(IMediator mediator)
        => _mediator = mediator;
​
    [HttpPost("{cartId}")]
    public async Task<IActionResult> AddProductToCart(Guid cartId, [FromBody] CartProduct cartProduct)
    {await _mediator.Send(new AddProductToCartCommand(cartId, cartProduct.Sku, cartProduct.Amount));
        return Ok();}
}

MediatR 验证

咱们将应用 MediatR 管道,而不是在控制器中验证 AddProductToCartCommand。

通过应用管道,能够在处理程序解决命令之前或之后执行一些逻辑。在这种状况下,提供一个集中的地位,在命令达到处理程序 (畛域) 之前在该地位对其进行验证。当命令达到它的处理程序时,咱们不再须要放心命令是否无效。

尽管这看起来是一个微不足道的更改,但它清理了畛域层中每个处理程序。

现实状况下,咱们只心愿在畛域中解决业务逻辑。删除验证逻辑解放了咱们的思维,这样咱们就能够更关注业务逻辑。因为验证逻辑是集中的,它确保所有命令都失去验证,而没有一条命令漏过破绽。

在上面的代码片段中,咱们创立了一个 ValidatorPipelineBehavior 来验证命令。当命令被发送时,ValidatorPipelineBehavior 处理程序在它达到畛域层之前接管命令。ValidatorPipelineBehavior 通过调用对应于该类型的验证器来验证该命令是否无效。只有当申请无效时,才容许将申请传递给下一个处理程序。如果没有,则抛出 InputValidationException 异样。

咱们将看看如何应用 FluentValidation 在验证中创立验证器。当初,重要的是要晓得,当申请有效时,将返回验证音讯。验证的细节被增加到异样中,稍后将用于创立响应。

public class ValidatorPipelineBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;
​
    public ValidatorPipelineBehavior(IEnumerable<IValidator<TRequest>> validators)
      => _validators = validators;
​
    public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        // Invoke the validators
        var failures = _validators
            .Select(validator => validator.Validate(request))
            .SelectMany(result => result.Errors)
            .ToArray();
​
        if (failures.Length > 0)
        {
            // Map the validation failures and throw an error,
            // this stops the execution of the request
            var errors = failures
                .GroupBy(x => x.PropertyName)
                .ToDictionary(k => k.Key, v => v.Select(x => x.ErrorMessage).ToArray());
            throw new InputValidationException(errors);
        }
​
        // Invoke the next handler
        // (can be another pipeline behavior or the request handler)
        return next();}
}

应用 FluentValidation 进行验证

为了验证申请,我喜爱应用 FluentValidation 库。应用 FluentValidation,通过实现 AbstractValidator 抽象类来为每个“IRequest”定义“验证规定”。

我喜爱应用 FluentValidation 的起因是:

  • 验证规定与模型是拆散的
  • 易写易读
  • 除了许多内置验证器之外,还能够创立本人的 (可重用的) 自定义规定
  • 可扩展性
public class AddProductToCartCommandValidator : FluentValidation.AbstractValidator<AddProductToCartCommandCommand>
{public AddProductToCartCommandValidator()
    {RuleFor(x => x.CartId)
            .NotEmpty();
​
        RuleFor(x => x.Sku)
            .NotEmpty();
​
        RuleFor(x => x.Amount)
            .GreaterThan(0);
    }
}

注册 MediatR 和FluentValidation

当初咱们有了验证的办法,也创立了一个验证器,咱们能够把它们注册到 DI 容器中。

public void ConfigureServices(IServiceCollection services)
{services.AddControllers();
​
    // Register all Mediatr Handlers
    services.AddMediatR(typeof(Startup));
​
    // Register custom pipeline behaviors
    services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidatorPipelineBehavior<,>));
​
    // Register all Fluent Validators
    services
        .AddMvc()
        .AddFluentValidation(s => s.RegisterValidatorsFromAssemblyContaining<Startup>());
}

HTTP API 问题详细信息

当初所有都筹备好了,能够收回第一个申请了。当咱们尝试发送一个有效申请时,咱们会收到一个外部服务器谬误 (500) 响应。这很好,但这并不是的良好体验。

为了给用户(用户界面)、开发人员(或者你本人),甚至是第三方发明更好的体验,优化后的后果将使申请失败的起因变得清晰。这种做法使与 API 的集成更容易、更好,而且可能更快。

当我不得不与第三方服务集成,他们却没有思考到这一点。这导致了我的许多挫折,当整合最终完结时,我很快乐。我确信,如果能更多的思考对失败申请的响应,实现会更快,最终后果也会更好。遗憾的是,大多数与第三方服务的集成都是蹩脚的体验。

因为这次经验,我尽最大的致力通过提供更好的响应来帮忙将来的本人和其余开发者。更好的操作是,一个标准化的响应,我称为 HTTP api 的问题详细信息。

. net 框架曾经提供了一个类来实现问题详细信息的标准,即 ProblemDetails。事实上,. net API 会为一些有效的申请返回一个问题详细信息响应。例如,当在路由中应用了一个有效参数时,. net 返回如下响应。

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "00-6aac4e84d1d4054f92ac1d4334c48902-25e69ea91f518045-00",
  "errors": {"id": ["The value'one'is not valid."]
  }
}

将响应 (异样) 映射到问题详细信息

为了标准咱们的问题详细信息,能够用异样中间件或异样过滤器重写响应。

在上面的代码片段中,当应用程序中出现异常时,咱们将应用中间件检索异样的详细信息。依据这些异样详细信息,构建问题详细信息对象。

所有抛出的异样都由中间件捕捉,因而你能够为每个异样创立特定的问题详细信息。在上面的例子中,只有 InputValidationException 异样被映射,其余的异样都被同等对待。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseExceptionHandler(errorApp =>
    {
        errorApp.Run(async context =>
        {var errorFeature = context.Features.Get<IExceptionHandlerFeature>();
            var exception = errorFeature.Error;
​
            // https://tools.ietf.org/html/rfc7807#section-3.1
            var problemDetails = new ProblemDetails
            {Type = $"https://example.com/problem-types/{exception.GetType().Name}",
                Title = "An unexpected error occurred!",
                Detail = "Something went wrong",
                Instance = errorFeature switch
                {
                    ExceptionHandlerFeature e => e.Path,
                    _ => "unknown"
                },
                Status = StatusCodes.Status400BadRequest,
                Extensions =
                {["trace"] = Activity.Current?.Id ?? context?.TraceIdentifier
                }
            };
​
            switch (exception)
            {
                case InputValidationException validationException:
                    problemDetails.Status = StatusCodes.Status403Forbidden;
                    problemDetails.Title = "One or more validation errors occurred";
                    problemDetails.Detail = "The request contains invalid parameters. More information can be found in the errors.";
                    problemDetails.Extensions["errors"] = validationException.Errors;
                    break;
            }
​
            context.Response.ContentType = "application/problem+json";
            context.Response.StatusCode = problemDetails.Status.Value;
            context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
            {NoCache = true,};
            await JsonSerializer.SerializeAsync(context.Response.Body, problemDetails);
        });
    });
​
    app.UseHttpsRedirection();
    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {endpoints.MapControllers();
    });
}

有了异样处理程序,当检测到有效命令时,将返回以下响应。例如,当 AddProductToCartCommand 命令 (参见 MediatR 命令) 以正数发送时。

{
  "type": "https://example.com/problem-types/InputValidationException",
  "title": "One or more validation errors occurred",
  "status": 403,
  "detail": "The request contains invalid parameters. More information can be found in the errors.",
  "instance": "/customercarts",
  "trace": "00-22fde64da9b70a4691e8c536aafb2c49-f90b88a19f1dca47-00",
  "errors": {"Amount": ["'Amount' must be greater than '0'."]
  }
}

除了创立自定义异样处理程序并将异样映射到问题详细信息之外,还能够应用 Hellang.Middleware.ProblemDetails 包。Hellang.Middleware.ProblemDetails 包能够很容易地将异样映射到问题详细信息,简直不须要任何代码。

统一的问题详细信息

还有最初一个问题。下面的代码片段冀望应用程序在控制器中创立 MediatR 申请。在 body 中蕴含该命令的 API 终结点将主动被. net 模型验证器验证。当终结点接管到有效命令时,咱们的管道和异样解决不会解决申请。这意味着将返回默认的. net 响应,而不是咱们的问题详细信息。

例如,AddProductToCart 间接接管 AddProductToCartCommand 命令,并将该命令发送到 MediatR 管道。

[ApiController]
[Route("[controller]")]
public class CustomerCartsController : ControllerBase
{
    private readonly IMediator _mediator;
​
    public CustomerCartsController(IMediator mediator)
        => _mediator = mediator;
​
    [HttpPost]
    public async Task<IActionResult> AddProductToCart(AddProductToCartCommand command)
    {await _mediator.Send(command);
        return Ok();}
}

我一开始并没有预料到这一点,花了一段时间才弄清楚为什么会产生这种状况,以及如何确保响应对象保持一致。作为一种可能的修复,咱们能够克制这种默认行为,这样有效的申请将由咱们的管道解决。

public void ConfigureServices(IServiceCollection services)
{services.AddControllers();
​
    // Register all Mediatr Handlers
    services.AddMediatR(typeof(Startup));
​
    // Register custom pipeline behaviors
    services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidatorPipelineBehavior<,>));
​
    // Register all Fluent Validators
    services
        .AddMvc()
        .AddFluentValidation(s => s.RegisterValidatorsFromAssemblyContaining<Startup>());
​
    services.Configure<ApiBehaviorOptions>(options => {options.SuppressModelStateInvalidFilter = true;});
}

但这也有一个毛病。不能捕捉有效的数据类型。因而,敞开有效的模型过滤器可能会导致意想不到的谬误。以前,这个操作会导致一个 bad request(400)。这就是为什么我更喜爱接管到谬误输出时抛出 InputValidationException 异样。

public void ConfigureServices(IServiceCollection services)
{services.AddControllers();
​
    // Register all Mediatr Handlers
    services.AddMediatR(typeof(Startup));
​
    // Register custom pipeline behaviors
    services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidatorPipelineBehavior<,>));
​
    // Register all Fluent Validators
    services
        .AddMvc()
        .AddFluentValidation(s => s.RegisterValidatorsFromAssemblyContaining<Startup>());
​
    services.Configure<ApiBehaviorOptions>(options => {
        options.InvalidModelStateResponseFactory = context => {var problemDetails = new ValidationProblemDetails(context.ModelState);
            throw new InputValidationException(problemDetails.Errors);
        };
    });
}

总结

在这篇文章中,咱们曾经看到了如何通过 MediatR 管道行为在命令达到畛域层之前集中验证逻辑。这样做的益处是,所有的命令都是无效的,当一个命令达到它的处理程序时,它将是无效的。换句话说,畛域将放弃洁净和简略。

因为有一个清晰的拆散,开发人员只须要关注不言而喻的工作。在开发过程中,还能够保障单元测试更有针对性,也更容易编写。

未来,如果需要的话,还能够更容易地替换验证层。

欢送关注我的公众号,如果你有喜爱的外文技术文章,能够通过公众号留言举荐给我。

退出移动版