​开发一款成功软件的关键是良好的架构设计。优秀的设计不仅允许开发人员轻松地编写新功能,而且还能丝滑的适应各种变化。

好的设计应该关注应用程序的核心,即领域。

不幸的是,这很容易将领域与不属于这一层的职责混淆。每增加一个功能,就会使理解核心领域变得更加困难。同样糟糕的是,将来就更难重构了。

因此,保护领域层不受应用程序逻辑影响是很重要的。其中一个优化是对传入请求的验证。为了防止验证逻辑渗透到领域级别,我们希望在请求到达领域级别之前验证请求。

在这篇文章中,我们将学习如何从领域层中提取验证。在我们开始之前,本文假设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管道行为在命令到达领域层之前集中验证逻辑。这样做的好处是,所有的命令都是有效的,当一个命令到达它的处理程序时,它将是有效的。换句话说,领域将保持干净和简单。

因为有一个清晰的分离,开发人员只需要关注显而易见的任务。在开发过程中,还可以保证单元测试更有针对性,也更容易编写。

将来,如果需要的话,还可以更容易地替换验证层。

欢迎关注我的公众号,如果你有喜欢的外文技术文章,可以通过公众号留言推荐给我。

原文链接:https://timdeschryver.dev/blog/creating-a-new-csharp-api-validate-incoming-requests

如何创建一个验证请求的API框架的更多相关文章

  1. 依赖注入[4]: 创建一个简易版的DI框架[上篇]

    本系列文章旨在剖析.NET Core的依赖注入框架的实现原理,到目前为止我们通过三篇文章(<控制反转>.<基于IoC的设计模式>和< 依赖注入模式>)从纯理论的角度 ...

  2. .NET CORE学习笔记系列(2)——依赖注入[4]: 创建一个简易版的DI框架[上篇]

    原文https://www.cnblogs.com/artech/p/net-core-di-04.html 本系列文章旨在剖析.NET Core的依赖注入框架的实现原理,到目前为止我们通过三篇文章从 ...

  3. 依赖注入[5]: 创建一个简易版的DI框架[下篇]

    为了让读者朋友们能够对.NET Core DI框架的实现原理具有一个深刻而认识,我们采用与之类似的设计构架了一个名为Cat的DI框架.在<依赖注入[4]: 创建一个简易版的DI框架[上篇]> ...

  4. 在一个空ASP.NET Web项目上创建一个ASP.NET Web API 2.0应用

    由于ASP.NET Web API具有与ASP.NET MVC类似的编程方式,再加上目前市面上专门介绍ASP.NET Web API 的书籍少之又少(我们看到的相关内容往往是某本介绍ASP.NET M ...

  5. API Star:一个 Python 3 的 API 框架

    为了在 Python 中快速构建 API,我主要依赖于 Flask.最近我遇到了一个名为 "API Star" 的基于 Python 3 的新 API 框架.由于几个原因,我对它很 ...

  6. 如何创建一个Asp .Net Web Api项目

    1.点击文件=>新建=>项目 2.创建一个Asp .NET Web项目 3.选择Empty,然后选中下面的MVC和Web Api,也可以直接选择Web Api选项,注意将身份验证设置为无身 ...

  7. .NET CORE学习笔记系列(2)——依赖注入[5]: 创建一个简易版的DI框架[下篇]

    为了让读者朋友们能够对.NET Core DI框架的实现原理具有一个深刻而认识,我们采用与之类似的设计构架了一个名为Cat的DI框架.在上篇中我们介绍了Cat的基本编程模式,接下来我们就来聊聊Cat的 ...

  8. Jmeter创建一个http请求

    1.点击'Test Plan'为测试计划命名为"创建用户接口" 2.新建一个[线程组],在[创建用户接口]处点击右键,选择[添加]-->[Threads(Users)]--& ...

  9. Jmeter5.1.1创建一个http请求的压力测试

    1.首先添加一个线程组,在线程组中,配置压力情况 2.然后在线程组中,添加取样器,添加http请求:配置web服务器协议(http/https).服务器名称或IP.端口号.请求方法.路径等参数 3.然 ...

随机推荐

  1. java基础:switch语句应用,循环的详细介绍以及使用,附练习案列

    1. switch语句 1.1 分支语句switch语句 格式 switch (表达式) { case 1: 语句体1; break; case 2: 语句体2; break; ... default ...

  2. NuGet 学习笔记(1)--Nuget安装使用

    安装NuGet扩展 要使用NuGet首先需要安装它(vs2013NuGet) 1. 点击 工具(Tools)-->扩展管理器(Extensions and Updates)...-->右上 ...

  3. 嵌入式LInux-让开发板访问外网-ping bad address baidu.com

    我的嵌入式设备已经接入网络.能够ping局域网ip.可是为了实现能够ping通外网.比如 ping baidu.com 还是不行的. 当运行ping baidu.com这个命令时,提示 ping ba ...

  4. http协议中的缓存机制

    强缓存 - expires,服务器给客户端一个过期日期,如(2020-12-12),过了该时间,客户端请求服务器重新获取.存在问题:客户端与服务端存在时间差,会导致过期时间不准确 - Cache-co ...

  5. 我是这样理解EventLoop的

    我是这样理解EventLoop的 一.前言   众所周知,在使用javascript时,经常需要考虑程序中存在异步的情况,如果对异步考虑不周,很容易在开发中出现技术错误和业务错误.作为一名合格的jav ...

  6. MySql Docker 主主配置

    MySql 主主 准备2台Linux服务器,并且在两台服务器上,同时安装docker,国内的同学可以使用aliyun的镜像安装. curl -fsSL https://get.docker.com - ...

  7. sql操作数据库(2)--->DQL、数据库备份和还原

    查询 查询表中的所有的行和列的数据 ​ select * from 表名; ​ select * from student; 查询指定列的数据:如果有多个列,中间用逗号隔开. select 列名1,列 ...

  8. Redis学习之路(二)Redis集群搭建

    一.Redis集群搭建说明 基于三台虚拟机部署9个节点,一台虚拟机三个节点,创建出4个master.4个slave的Redis集群. Redis 集群搭建规划,由于集群至少需要6个节点(3主3从模式) ...

  9. PHPExcel-Helper快速构建Excel

    项目介绍 PHPExcel-Helper是什么? PHPExcel辅助开发类,帮助开发者快速创建各类excel. github PHPExcel-Helper存在的意义? 官方phpexcel库功能全 ...

  10. Redis核心原理-简单动态字符串SDS

    SDS简介 Redis是C语言编写的,但没有使用c语言的字符串结构,而是自己实现了一套简单动态字符串 simple dynamic string 简称SDS,SDS兼容C语言的字符串类型,原理类似Ja ...