.NET 6开发TodoList应用之实现接口请求验证

目录
  • 需求
  • 目标
  • 原理与思路
  • 实现
  • 验证
  • 一点扩展
  • 总结
  • 参考资料

需求

在响应请求处理的过程中,我们经常需要对请求参数的合法性进行校验,如果参数不合法,将不继续进行业务逻辑的处理。我们当然可以将每个接口的参数校验逻辑写到对应的Handle方法中,但是更好的做法是借助MediatR提供的特性,将这部分与实际业务逻辑无关的代码整理到单独的地方进行管理。

为了实现这个需求,我们需要结合FluentValidation和MediatR提供的特性。

目标

将请求的参数校验逻辑从CQRS的Handler中分离到MediatR的Pipeline框架中处理。

原理与思路

MediatR不仅提供了用于实现CQRS的框架,还提供了IPipelineBehavior<TRequest, TResult>接口用于实现CQRS响应之前进行一系列的与实际业务逻辑不紧密相关的特性,诸如请求日志、参数校验、异常处理、授权、性能监控等等功能。

在本文中我们将结合FluentValidation和IPipelineBehavior<TRequest, TResult>实现对请求参数的校验功能。

实现

添加MediatR参数校验Pipeline Behavior框架支持#

首先向Application项目中引入FluentValidation.DependencyInjectionExtensionsNuget包。为了抽象所有的校验异常,先创建ValidationException类:

ValidationException.cs

namespace TodoList.Application.Common.Exceptions;

public class ValidationException : Exception
{
    public ValidationException() : base("One or more validation failures have occurred.")
    {
    }

    public ValidationException(string failures)
        : base(failures)
    {
    }
}

参数校验的基础框架我们创建到Application/Common/Behaviors/中:

ValidationBehaviour.cs

using FluentValidation;
using FluentValidation.Results;
using MediatR;
using ValidationException = TodoList.Application.Common.Exceptions.ValidationException;

namespace TodoList.Application.Common.Behaviors;

public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    // 注入所有自定义的Validators
    public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators)
        => _validators = validators;

    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        if (_validators.Any())
        {
            var context = new ValidationContext<TRequest>(request);

            var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken)));

            var failures = validationResults
                .Where(r => r.Errors.Any())
                .SelectMany(r => r.Errors)
                .ToList();

            // 如果有validator校验失败,抛出异常,这里的异常是我们自定义的包装类型
            if (failures.Any())
                throw new ValidationException(GetValidationErrorMessage(failures));
        }
        return await next();
    }

    // 格式化校验失败消息
    private string GetValidationErrorMessage(IEnumerable<ValidationFailure> failures)
    {
        var failureDict = failures
            .GroupBy(e => e.PropertyName, e => e.ErrorMessage)
            .ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray());

        return string.Join(";", failureDict.Select(kv => kv.Key + ": " + string.Join(' ', kv.Value.ToArray())));
    }
}

在DependencyInjection中进行依赖注入:

DependencyInjection.cs

// 省略其他...
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>) 

添加Validation Pipeline Behavior

接下来我们以添加TodoItem接口为例,在Application/TodoItems/CreateTodoItem/中创建CreateTodoItemCommandValidator:

CreateTodoItemCommandValidator.cs

using FluentValidation;
using Microsoft.EntityFrameworkCore;
using TodoList.Application.Common.Interfaces;
using TodoList.Domain.Entities;

namespace TodoList.Application.TodoItems.Commands.CreateTodoItem;

public class CreateTodoItemCommandValidator : AbstractValidator<CreateTodoItemCommand>
{
    private readonly IRepository<TodoItem> _repository;

    public CreateTodoItemCommandValidator(IRepository<TodoItem> repository)
    {
        _repository = repository;

        // 我们把最大长度限制到10,以便更好地验证这个校验
        // 更多的用法请参考FluentValidation官方文档
        RuleFor(v => v.Title)
            .MaximumLength(10).WithMessage("TodoItem title must not exceed 10 characters.").WithSeverity(Severity.Warning)
            .NotEmpty().WithMessage("Title is required.").WithSeverity(Severity.Error)
            .MustAsync(BeUniqueTitle).WithMessage("The specified title already exists.").WithSeverity(Severity.Warning);
    }

    public async Task<bool> BeUniqueTitle(string title, CancellationToken cancellationToken)
    {
        return await _repository.GetAsQueryable().AllAsync(l => l.Title != title, cancellationToken);
    }
}

其他接口的参数校验添加方法与此类似,不再继续演示。

验证

启动Api项目,我们用一个校验会失败的请求去创建TodoItem:

请求

响应

因为之前测试的时候已经在没有加校验的时候用同样的请求生成了一个TodoItem,所以校验失败的消息里有两项校验都没有满足。

一点扩展

我们在前文中说了使用MediatR的PipelineBehavior可以实现在CQRS请求前执行一些逻辑,其中就包含了日志记录,这里就把实现方式也放在下面,在这里我们使用的是Pipeline里的IRequestPreProcessor<TRequest>接口实现,因为只关心请求处理前的信息,如果关心请求处理返回后的信息,那么和前文一样,需要实现IPipelineBehavior<TRequest, TResponse>接口并在Handle中返回response对象:

// 省略其他...
var response = await next();
//Response
_logger.LogInformation($"Handled {typeof(TResponse).Name}");

return response;

创建一个LoggingBehavior:

using System.Reflection;
using MediatR.Pipeline;
using Microsoft.Extensions.Logging;

public class LoggingBehaviour<TRequest> : IRequestPreProcessor<TRequest> where TRequest : notnull
{
    private readonly ILogger<LoggingBehaviour<TRequest>> _logger;

    // 在构造函数中后面我们还可以注入类似ICurrentUser和IIdentity相关的对象进行日志输出
    public LoggingBehaviour(ILogger<LoggingBehaviour<TRequest>> logger)
    {
        _logger = logger;
    }

    public async Task Process(TRequest request, CancellationToken cancellationToken)
    {
        // 你可以在这里log关于请求的任何信息
        _logger.LogInformation($"Handling {typeof(TRequest).Name}");

        IList<PropertyInfo> props = new List<PropertyInfo>(request.GetType().GetProperties());
        foreach (var prop in props)
        {
            var propValue = prop.GetValue(request, null);
            _logger.LogInformation("{Property} : {@Value}", prop.Name, propValue);
        }
    }
}

如果是实现IPipelineBehavior<TRequest, TResponse>接口,最后注入即可。

// 省略其他...
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehaviour<,>));

如果实现IRequestPreProcessor<TRequest>接口,则不需要再进行注入。

效果如下图所示:

可以看到日志中已经输出了Command名称和请求参数字段值。

总结

在本文中我们通过FluentValidation和MediatR实现了不侵入业务代码的请求参数校验逻辑,在下一篇文章中我们将介绍.NET开发中会经常用到的ActionFilters。

参考资料

FluentValidation

How to use MediatR Pipeline Behaviours 

到此这篇关于.NET 6开发TodoList应用之实现接口请求验证的文章就介绍到这了,更多相关.NET 6接口请求验证内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • .NET 6开发TodoList应用引入第三方日志库

    目录 1.需求 2.目标 3.原理和思路 4.实现 4.1日志配置实现 4.2主程序配置 4.3注入使用 5.验证 1.需求 在我们项目开发的过程中,使用.NET 6自带的日志系统有时是不能满足实际需求的,比如有的时候我们需要将日志输出到第三方平台上,最典型的应用就是在各种云平台上,为了集中管理日志和查询日志,通常会选择对应平台的日志SDK进行集成.使用Serilog提供的多种Sink,可以实现将日志写入不同云平台或者是非云平台的日志存储中去,这是我们这篇文章讲要研究的内容. 2.目标 我们将为

  • .NET 6开发TodoList应用之实现查询分页

    目录 需求 目标 原理与思路 实现 定义分页结果数据结构 添加对于分页结果的Mapping Profile 创建分页查询请求 创建查询Controller 验证 总结 需求 查询中有个非常常见的需求就是后端分页,实现的方式也不算复杂,所以我们本文仅仅演示一个后端查询分页的例子. 目标 实现分页查询返回. 原理与思路 对于分页查询而言,我们需要在请求中获取当前请求的是第几页,每页请求多少项数据.在返回值中需要告诉前端,当前这一页的所有数据项列表,总共的数据项有多少.为此我们可以定义一个包装类型,供

  • 使用.NET 6开发TodoList应用之领域实体创建原理和思路

    需求 上一篇文章中我们完成了数据存储服务的接入,从这一篇开始将正式进入业务逻辑部分的开发. 首先要定义和解决的问题是,根据TodoList项目的需求,我们应该设计怎样的数据实体,如何去进行操作? 长文预警!包含大量代码 目标 在本文中,我们希望达到以下几个目标: 定义领域实体: 通过数据库操作领域实体: 原理和思路 虽然TodoList是一个很简单的应用,业务逻辑并不复杂,至少在这个系列文章中我并不想使其过度复杂.但是我还是打算借此简单地涉及领域驱动开发(DDD)的基础概念. 首先比较明确的是,

  • .NET 6开发TodoList应用之使用MediatR实现POST请求

    目录 需求 目标 原理与思路 CQRS模式 中介者Mediator模式 MediatR 实现 引入MediatR 实现Post请求 验证 创建TodoList验证 创建TodoItem验证 总结 参考资料 需求 需求很简单:如何创建新的TodoList和TodoItem并持久化. 初学者按照教程去实现的话,应该分成以下几步:创建Controller并实现POST方法:实用传入的请求参数new一个数据库实体对象:调用IRepository<T>完成数据库的写入,最多会在中间加一层Service.

  • .NET 6开发TodoList应用之实现DELETE请求与HTTP请求幂等性

    目录 需求 目标 原理与思路 实现 验证 总结 需求 先说明一下关于原本想要去更新的PATCH请求的文章,从目前试验的情况来看,如果是按照.NET 6的项目结构(即只使用一个Program.cs完成程序初始化),那微软官方给出的文档目前还没有对应地更新,按照之前的方式进行JsonPatch的配置是不行的,目前已经有人在Github微软的官方文档Repo下提了ISSUE: .NET 6: JsonPatch in ASP.NET Core web API.并且因为PATCH的使用频率并不高,所以我

  • .NET 6开发TodoList应用之使用AutoMapper实现GET请求

    目录 需求 目标 原理与思路 实现 引入AutoMapper 实现GET请求 验证 获取所有TodoList列表 获取单个TodoList详情 填一个POST文章里的坑 总结 需求 需求很简单:实现GET请求获取业务数据.在这个阶段我们经常使用的类库是AutoMapper. 目标 合理组织并使用AutoMapper,完成GET请求. 原理与思路 首先来简单地介绍一下这这个类库. 关于AutoMapper 在业务侧代码和数据库实体打交道的过程中,一个必不可少的部分就是返回的数据类型转换.对于不同的

  • .NET 6开发TodoList应用之请求日志组件HttpLogging介绍

    背景 因为在上篇演示Action Filter的时候可能是因为举的例子不够好,有小伙伴在评论区指出.NET 6新增加的特性可以实现在视图模型绑定之前允许记录Http请求日志的组件:HttpLogging.这个组件我之前试过,而Action Filter与其用来记录日志,更不如说是为Http请求的接收和响应提供了中间可以修改的机会. 本着让更多的人了解新知识的出发点,这次我们临时把这个主题加进来. 什么是HttpLogging? HttpLogging 是 .NET 6 新加入的一个框架内置的中间

  • 使用.NET 6开发TodoList应用之引入数据存储的思路详解

    需求 作为后端CRUD程序员(bushi,数据存储是开发后端服务一个非常重要的组件.对我们的TodoList项目来说,自然也需要配置数据存储.目前的需求很简单: 需要能持久化TodoList对象并对其进行操作: 需要能持久化TodoItem对象并对其进行操作: 问题是,我们打算如何存储数据? 存储组件的选择非常多:以MSSQL Server/Postgres/MySql/SQLite等为代表的关系型数据库,以MongoDB/ElasticSearch等为代表的非关系型数据库,除此之外,我们还可以

  • .NET 6开发TodoList应用之实现ActionFilter

    目录 需求 目标 原理与思路 实现 验证 总结 需求 Filter在.NET Web API项目开发中也是很重要的一个概念,它运行在执行MVC响应的Pipeline中执行,允许我们将一些可以在多个Action之间重用的逻辑抽取出来集中管理.虽然我们在上一篇使用.NET 6开发TodoList应用之实现接口请求验证中演示了如何通过使用MediatR提供的IPipelineBehavior接口在CQRS的Handle方法执行前后插入可重用代码,而本文所演示的Filters作用在Controller的

  • .NET 6开发TodoList应用之实现Repository模式

    目录 需求 目标 原理和思路 实现 通用Repository实现 引入使用 验证 总结 需求 经常写CRUD程序的小伙伴们可能都经历过定义很多Repository接口,分别做对应的实现,依赖注入并使用的场景.有的时候会发现,很多分散的XXXXRepository的逻辑都是基本一致的,于是开始思考是否可以将这些操作抽象出去,当然是可以的,而且被抽象出去的部分是可以不加改变地在今后的任何有此需求的项目中直接引入使用. 那么我们本文的需求就是:如何实现一个可重用的Repository模块. 长文预警,

  • .NET 6开发TodoList应用实现系列背景

    目录 1.列说明 2.系列导航 2.1 使用.NET 6开发TodoList应用文章索引 2.1.1创建项目 2.1.2.NET 6 WebAPI Program.cs的变更 2.1.3Change 1: Top-level statements 2.1.4Change 2: Implicit using directives 2.1.5Change 3: No Startup class 2.2 关于Pipeline的一些知识点 2.2.1Pipeline Sequence 2.2.2app.

  • .NET 6开发TodoList应用实现结构搭建

    目录 1.TodoList需求简介 2.开发工具 2.1.NET 6 2.2Visual Studio Code 2.3Hoppscotch 3.Clean Architecture简介 4.搭建解决方案结构 5.运行 往期学习: .NET 6开发TodoList应用实现系列背景 1.TodoList需求简介 首先明确一下我们即将开发的这个TodoList应用都需要完成什么功能,我不会一次性把所有的特性诸如允许用户登陆之类的需求全部写上,只是先列出最基本的功能性需求: 我们可以维护一个TodoL

  • .NET 6开发TodoList应用引入数据存储

    目录 一.需求 二.目标 三.原理和思路 四.实现 1. 引入Nuget包并进行配置 2. 添加DBContext对象并进行配置# 3. 配置文件修改 4. 主程序配置 5. 本地运行MSSQL Server容器及数据持久化 五.验证 一.需求 作为后端CRUD程序员(bushi,数据存储是开发后端服务一个非常重要的组件.对我们的TodoList项目来说,自然也需要配置数据存储. 目前的需求很简单: 需要能持久化TodoList对象并对其进行操作: 需要能持久化TodoItem对象并对其进行操作

  • .NET 6开发TodoList应用之实现全局异常处理

    目录 需求 目标 原理和思路 实现 验证 总结 参考资料 需求 因为在项目中,会有各种各样的领域异常或系统异常被抛出来,那么在Controller里就需要进行完整的try-catch捕获,并根据是否有异常抛出重新包装返回值.这是一项机械且繁琐的工作.有没有办法让框架自己去做这件事呢? 有的,解决方案的名称叫做全局异常处理,或者叫做如何让接口优雅地失败. 目标 我们希望将异常处理和消息返回放到框架中进行统一处理,摆脱Controller层的try-catch块. 原理和思路 一般而言用来实现全局异

  • .NET 6开发TodoList应用之实现PUT请求

    目录 需求 目标 原理与思路 实现 PUT请求 领域事件的发布和响应 验证 总结 需求 PUT请求本身其实可说的并不多,过程也和创建基本类似.在这篇文章中,重点是填上之前文章里留的一个坑,我们曾经给TodoItem定义过一个标记完成的领域事件:TodoItemCompletedEvent,在SaveChangesAsync方法里做了一个DispatchEvents的操作.并且在DomainEventService实现IDomainEventService的Publish方法中暂时以下面的代码代替

随机推荐