1. 程式人生 > 程式設計 >.NET 6開發TodoList應用之實現介面請求驗證

.NET 6開發TodoList應用之實現介面請求驗證

目錄
  • 需求
  • 目標
  • 原理與思路
  • 實現
  • 驗證
  • 一點擴充套件
  • 總結
  • 參考資料

需求

在響應請求處理的過程中,我們經常需要對請求引數的合法性進行校驗,如果引數不合法,將不繼續進行業務邏輯的處理。我們當然可以將每個介面的引數校驗邏輯寫到對應的Handle方法中,但是更好的做法是藉助MediatR提供的特性,將這部分與實際業務邏輯無關的程式碼整理到單獨的地方進行管理。

為了實現這個需求,我們需要結合FluentValidation和MediatR提供的特性。

目標

將請求的引數校驗邏輯從CQRS的Handler中分離到MediatR的Pipeline框架中處理。

原理與思路

MediatR不僅提供了用於實現CQRS的框架,還提供了IPipelineBehavior<TRequest,TResult>介面用於實現CQRS響應之前進行一系列的與實際業務邏輯不緊密相關的特性,諸如請求日誌、www.cppcns.com

引數校驗、異常處理、授權、效能監控等等功能。

在本文中我們將結合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:

請求

.NET 6開發TodoList應用之實現介面請求驗證

響應

.NET 6開發TodoList應用之實現介面請求驗證

因為之前測試的時候已經在沒有加校驗的時候用同樣的請求生成了一個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.Exthttp://www.cppcns.comensions.Logging;

public class LoggingBehaviour<TRequest> : IRequestPreProcessor<TRequest> where TRequest wVQhIt: 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>介面,則不需要再進行注入。

效果如下圖所示:

.NET 6開發TodoList應用之實現介面請求驗證

可以看到日誌中已經輸出了Command名稱和請求引數欄位值。

總結

在本文中我們通過FluentValidation和MediatR實現了不侵入業務程式碼的請求引數校驗邏輯,在下一篇文章中我們將介紹.NET開發中會經常用到的ActionFilters。

參考資料

FluentValidation

How to use MediatR Pipeline Behaviours

到此這篇關於.NET 6開發TodoList應用之實現介面請求驗證的文章就介紹到這了,更多相關.NET 6介面請求驗證內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!