首页 > 编程语言 >使用MediatR和FluentValidation实现CQRS应用程序的数据验证

使用MediatR和FluentValidation实现CQRS应用程序的数据验证

时间:2023-08-25 18:55:22浏览次数:36  
标签:exception MediatR 验证 FluentValidation 应用程序 CQRS public

本文将重点介绍如何通过MediatR的管道功能将FluentValidation集成到项目中实现验证功能。

什么是CQRS?

CQRS(Command Query Responsibility Segregation)也叫命令查询职责分离,是近年来非常流行的应用程序架构模式。CQRS 背后的理念是在逻辑上将应用程序的流程分成两个独立的流程,即命令或查询。

命令用于改变应用程序的状态。对应CRUD的创建、更新和删除部分。查询用于检索应用程序中的信息,对应CRUD的读取部分。

CQRS 的优缺点

优点:

  • 单一职责 – 命令和查询只有一个职责。要么更改应用程序的状态,要么检索它。因此它们很容易推理和理解。
  • 解耦 – 命令或查询与其处理程序完全解耦,因此在处理程序方面有很大的灵活性,可以按照自己认为最合适的方式来实现。
  • 可扩展性 – CQRS 模式在如何组织数据存储方面非常灵活,为您提供了多种可扩展性选择。您可以将一个数据库用于命令和
    查询。您可以使用独立的读/写数据库来提高性能,并在数据库之间使用消息传递或复制来实现同步。
  • 可测试性 – 测试命令或查询处理程序非常简单,因为它们的设计非常简单,只执行一项任务。

缺点:

  • 复杂性 – CQRS 是一种高级设计模式,您需要花时间才能完全理解它。它引入了很多复杂性,会给项目带来摩擦和潜在问题。在决定在项目中使用之前,请务必考虑清楚。
  • 学习曲线 – 虽然 CQRS 看起来是一种简单明了的设计模式,但仍存在学习曲线。大多数开发人员习惯于用过程式(命令式)风格编写代码,而 CQRS 则与之大相径庭。
  • 难以调试 – 由于命令和查询与其处理程序是分离的,因此应用程序没有自然的命令式流程。这使得它比传统应用程序更难调试。

使用 MediatR 的命令和查询

MediatR 使用接口(interface)来表示命令和查询。在我们的项目中,我们将为命令和查询创建单独的抽象。

首先,让我们看看接口是如何定义的:

using MediatR;
namespace Application.Abstractions.Messaging
{
    public interface ICommand<out TResponse> : IRequest<TResponse>
    {
    }
}
using MediatR;
namespace Application.Abstractions.Messaging
{
    public interface IQuery<out TResponse> : IRequest<TResponse>
    {
    }
}

我们在声明TResponse泛型时使用了 out 关键字,这表示它是协变的。这样,我们就可以使用比泛型参数指定的类型更多的派生类型。要了解有关协变和逆变的更多信息,请查看微软文档。

此外,为了完整起见,我们需要对命令和查询处理程序进行单独的抽象。

using MediatR;
namespace Application.Abstractions.Messaging
{
    public interface ICommandHandler<in TCommand, TResponse> : IRequestHandler<TCommand, TResponse>
        where TCommand : ICommand<TResponse>
    {
    }
}
using MediatR;
namespace Application.Abstractions.Messaging
{
    public interface IQueryHandler<in TQuery, TResponse> : IRequestHandler<TQuery, TResponse>
        where TQuery : IQuery<TResponse>
    {
    }
}

这里留下一个小问题,MediatR已经提供了IRequestIRequest<TResponse>两个接口,那我们为什么还要再次定义IQuery<out TResponse>ICommand<out TResponse>呢?

使用FluentValidation进行验证

FluentValidation 库允许我们轻松地为我们的类定义非常丰富的自定义验证。由于我们正在实现 CQRS,所以这里我们仅讨论对Command进行验证。由于Query对象仅仅是从应用程序获取数据,意思我们不必多此一举为Query设计验证器。

我们先设计一个UpdateUserCommand

public sealed record UpdateUserCommand(int UserId, string FirstName, string LastName) : ICommand<Unit>;

Unit是MediatR定义的一个特殊类,表示请求不返回数据,相当于voidTask

这个命令将用于更新已有用户(通过UserId查找)的FirstName和LastName,关于MediatR如何新增、查询和修改数据,在之前的文章中我们已经介绍过了,这里不再赘述。

接下来我们需要为UpdateUserCommand定义一个验证器:

public sealed class UpdateUserCommandValidator : AbstractValidator<UpdateUserCommand>
{
    public UpdateUserCommandValidator()
    {
        RuleFor(x => x.UserId).NotEmpty();
        RuleFor(x => x.FirstName).NotEmpty().MaximumLength(100);
        RuleFor(x => x.LastName).NotEmpty().MaximumLength(100);
    }
}

此验证器将对UpdateUserCommand的属性进行以下验证:

  • UserId - 不可空
  • FirstName - 不可空且最大长度不超过100个字符
  • LastName - 不可空且最大长度不超过100个字符

使用 MediatR PipelineBehavior创建装饰器

CQRS 模式使用命令和查询来传达信息并接收响应。实质上是请求-响应管道。这使我们能够轻松地围绕通过管道的每个请求引入其他行为,而无需实际修改原始请求。

您可能熟悉这种名为装饰器模式的技术。使用装饰器模式的典型例子就是ASP.NET Core中间件。MediatR与中间件的概念类似,称为:IPipelineBehavior

public interface IPipelineBehavior<in TRequest, TResponse> where TRequest : notnull
{
    Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next);
}

PipelineBehavior是请求实例的包装器,在如何实现它方面为您提供了很大的灵活性。PipelineBehavior非常适合应用程序中的横切关注点。横切关注点的很好的例子是日志记录、缓存,当然还有验证!

创建验证PipelineBehavior

为了在 CQRS 管道中实现验证,我们将使用刚才谈到的概念,即 MediatR 的 IPipelineBehavior 和 FluentValidation。

首先我们创建一个ValidationBehavior

public sealed class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : class, ICommand<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;
    
    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators) => _validators = validators;
    
    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        if (!_validators.Any())
        {
            return await next();
        }
        var context = new ValidationContext<TRequest>(request);
        var errorsDictionary = _validators
            .Select(x => x.Validate(context))
            .SelectMany(x => x.Errors)
            .Where(x => x != null)
            .GroupBy(
                x => x.PropertyName,
                x => x.ErrorMessage,
                (propertyName, errorMessages) => new
                {
                    Key = propertyName,
                    Values = errorMessages.Distinct().ToArray()
                })
            .ToDictionary(x => x.Key, x => x.Values);
        if (errorsDictionary.Any())
        {
            throw new ValidationException(errorsDictionary);
        }
        return await next();
    }
}

处理验证异常

为了处理遇到验证错误时抛出的ValidationException,我们可以使用 ASP.NET Core的 IMiddleware接口。

internal sealed class ExceptionHandlingMiddleware : IMiddleware
{
    private readonly ILogger<ExceptionHandlingMiddleware> _logger;
    
    public ExceptionHandlingMiddleware(ILogger<ExceptionHandlingMiddleware> logger) => _logger = logger;
    
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        try
        {
            await next(context);
        }
        catch (Exception e)
        {
            _logger.LogError(e, e.Message);
            await HandleExceptionAsync(context, e);
        }
    }
    
    private static async Task HandleExceptionAsync(HttpContext httpContext, Exception exception)
    {
        var statusCode = GetStatusCode(exception);
        var response = new
        {
            title = GetTitle(exception),
            status = statusCode,
            detail = exception.Message,
            errors = GetErrors(exception)
        };
        httpContext.Response.ContentType = "application/json";
        httpContext.Response.StatusCode = statusCode;
        await httpContext.Response.WriteAsync(JsonSerializer.Serialize(response));
    }
    private static int GetStatusCode(Exception exception) =>
        exception switch
        {
            BadRequestException => StatusCodes.Status400BadRequest,
            NotFoundException => StatusCodes.Status404NotFound,
            ValidationException => StatusCodes.Status422UnprocessableEnttity,
            _ => StatusCodes.Status500InternalServerError
        };
    private static string GetTitle(Exception exception) =>
        exception switch
        {
            ApplicationException applicationException => applicationException.Title,
            _ => "Server Error"
        };
    private static IReadOnlyDictionary<string, string[]> GetErrors(Exception exception)
    {
        IReadOnlyDictionary<string, string[]> errors = null;
        if (exception is ValidationException validationException)
        {
            errors = validationException.ErrorsDictionary;
        }
        return errors;
    }
}

设置依赖注入

在运行应用程序之前,我们需要确保已向 DI 容器注册了所有服务。MediatR的DI注入方式之前已经介绍过,这里主要演示FluentValidation的注入。由于ValidationBehavior依赖IValidator<T>,因此需要注入我们定义的Validator。

// 在Startup.cs中配置
services.AddValidatorsFromAssembly(typeof(Application.AssemblyReference).Assembly);

// 在Program.cs中配置(≥ net 6.0)
builder.Services.AddValidatorsFromAssembly(typeof(Application.AssemblyReference).Assembly);

最后我们需要将ExceptionHandlingMiddleware也注册到DI容器和ASP.NET Core的管道中:

// 在Startup.cs中配置
services.AddTransient<ExceptionHandlingMiddleware>();

// 在Program.cs中配置(≥ net 6.0)
builder.Services.AddTransient<ExceptionHandlingMiddleware>();

app.UseMiddleware<ExceptionHandlingMiddleware>();

测试验证管道

在项目的Controllers文件夹中找到UserController:

/// <summary>
/// The users controller.
/// </summary>
[ApiController]
[Route("api/[controller]")]
public sealed class UsersController : ControllerBase
{
    private readonly ISender _sender;
    
    /// <summary>
    /// Initializes a new instance of the <see cref="UsersController"/> class.
    /// </summary>
    /// <param name="sender"></param>
    public UsersController(ISender sender) => _sender = sender;
    
    /// <summary>
    /// Updates the user with the specified identifier based on the specified request, if it exists.
    /// </summary>
    /// <param name="userId">The user identifier.</param>
    /// <param name="request">The update user request.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>No content.</returns>
    [HttpPut("{userId:int}")]
    public async Task<IActionResult> UpdateUser(int userId, [FromBody] UpdateUserRequest request, CancellationToken cancellationToken)
    {
        var command = request.Adapt<UpdateUserCommand>() with
        {
            UserId = userId
        };
        await _sender.Send(command, cancellationToken);
        return NoContent();
    }
}

我们可以看到,UpdateUser 操作非常简单,它从路由中获取用户Id,从请求正文中获取FirstName和LastName,然后创建一个新的 UpdateUserCommand实例并且通过管道发送命令。最后返回204(请求成功但无响应内容)状态码。

接下来我们通过Swagger调用API接口:

Swagger interface showing PUT method to update users resource.

可以看到,请求的FirstName和LastName都是空白字符串。

Swagger interface showing PUT method error response.

补充内容之后再次发送请求。

Swagger interface showing PUT method and response when user is not found.

结论

在本文中,我们介绍了CQRS 模式的一些更高级的概念,以及如何在应用程序中通过横切的方式实现数据验证,同时也简单的介绍了如何通过ASP.NTE Core的中间件实现全局异常处理。


点关注,不迷路。

如果您喜欢这篇文章,请不要忘记点赞、关注、转发,谢谢!如果您有任何高见,欢迎在评论区留言讨论……

公众号

 

转 https://www.cnblogs.com/zhaorong/p/fluent-validation-with-mediatr.html

标签:exception,MediatR,验证,FluentValidation,应用程序,CQRS,public
From: https://www.cnblogs.com/wl-blog/p/17657744.html

相关文章

  • 使用MediatR实现CQRS
    CQRS和中介者模式MediatR库主要是为了帮助开发者快速实现两种软件架构模式:CQRS和Mediator。这两种架构模式看上去似乎差不多,但还是有很多区别的。CQRSCQRS是CommandQueryResponsibilitySegregation的缩写,一般称作命令查询职责分离。从字面意思理解,就是将命令(写入)和查询(读取)......
  • Wow: 基于 DDD、EventSourcing 的现代响应式 CQRS 架构微服务开发框架
    领域驱动|事件驱动|测试驱动|声明式设计|响应式编程|命令查询职责分离|事件溯源架构图事件源可观测性OpenAPI(SpringWebFlux集成)自动注册命令路由处理函数(HandlerFunction),开发人员仅需编写领域模型,即可完成服务开发。测试套件:80%......
  • WPF使用FluentValidation进行表单验证
    WPF使用FluentValidation进行表单验证.net版本:6.0使用的NuGet包FluentValidation:11.6.0MaterialDesignThemes:4.9.0Prism.DryIoc:8.1.97在WPF里验证表单使用的是INotifyDataErrorInfo接口,这个接口长这样publicinterfaceINotifyDataErrorInfo{boolHasErrors{get;......
  • net core+mediatr+EF实现事件触发
    参考杨中科的教程1.先添加接口usingMediatR;namespaceNetOptions.Entities;publicinterfaceIDomainEnvent{voidAddNotification(INotificationnotification);IEnumerable<INotification>GetNotifications();voidClearNotifications();}2.添加抽......
  • Net Core-MediatR
    一GitHub地址:https://github.com/jbogard/MediatR二简单使用:publicclassMyRequestMsg:IRequest<int>{publicstringRequestMsgType{get;set;}=string.Empty;}publicclassMyRequestHandler:IRequestHandler<MyRequestMsg,int>{public......
  • net 6使用FluentValidation校验请求数据
    1,nuget增加FluentValidation.AspNetCore2.DI中添加builder.Services.AddFluentValidation(opt=>{opt.RegisterValidatorsFromAssembly(Assembly.GetEntryAssembly());});3.添加具体的校验类usingFluentValidation;namespaceEShopWebApi.Controllers;public......
  • 基于 .NET的FluentValidation验证教程
    虽然FluentValidation是一个非常强大的验证框架,但针对该框架的中文资料并不完善,零度在学习的过程中,将官方文档进行了翻译,由此产生本文,可供参阅。要使用验证框架,需要在项目中添加对FluentValidation.dll的引用,支持netstandard2.0库和.NET4.5平台,支持.NETCore平台,最简......
  • CQRS读写分离MySQL数据库如何部署至Linux
    FearlessGuo首先有一台可以使用的Linux服务器,可以自行购买,当然也可以白嫖。有一款可以连接Linux的软件,我用的是putty在Linux上下载docker镜像,类似应用商店。安装过程参阅下方链接Linux安装Docker完整教程_docker安装_风随心飞飞的博客-CSDN博客下载mysql镜像,查看版本本次......
  • 在.NetCore中 DDD中基于mediatr实现领域事件并结合EFCore进行二次封装
    [源代码地址https://github.com/junkai-li/NetCoreKevin]基于NET6搭建跨平台WebApi架构支持单点登录、多缓存、自动任务、分布式、多租户、日志、授权和鉴权、网关注册与发现、CAP集成事件、领域事件、docker部署**首要要理解什么是领域事件?**领域事件是指发生在特定领域中的......
  • Validating multiple properties with FluentValidation(FluentValidator多个相关属性
    Sourcelink:ValidatingmultiplepropertieswithFluentValidation|TheCodetripper(wordpress.com)publicclassFooBarRequestValidator:AbstractValidator<FooBarRequest>{privatereadonlyIRepositoryrepository;publicFooBarRequestValid......