Chuyển đến nội dung chính

MediatR: CQRS Command Validation

Validation là 1 phần quan trọng của API. Khi sử dụng Clean Architect hoặc bất kỳ design pattern hiện đại nào, chúng ta thường tách phần Validation và Business Logic ra thành những thành phần riêng biệt.

Lưu ý trong bài viết này, chúng ta chỉ nói về Input Validation. Chúng ta sẽ không tách Business Validation

Giả sử với đoạn code sau, nếu không validate PageIndex thì thỉnh thoảng đoạn code sẽ gây ra exception nếu PageIndex < 0

public class GetAuthorQueryHandler : IRequestHandler<GetAuthorQuery, List<AuthorModel>>
{
	private readonly IAuthorRepository _authorRepository;

	public GetAuthorQueryHandler(IAuthorRepository authorRepository)
	{
		_authorRepository = authorRepository;
	}

	public async Task<List<AuthorModel>> Handle(GetAuthorQuery request, CancellationToken cancellationToken)
	{
		if(request.PageIndex < 0) throw new Exception("PageIndex is invalid")
		return _authorRepository.GetAuthors(request.PageSize, request.PageIndex);
	}
}

Để phân tách phần Validation, chúng ta sẽ áp dụng mẫu thiết kế cross-cutting concern. Trong ASP.NET Core, điều này được thực hiện thông qua middleware (xử lý ngoại lệ, xác thực, CORS, định tuyến, vv.). Trong MediatR, chúng ta sử dụng IPipelineBehaviour để thực hiện điều này.

Pipeline Behavior

Trong MediatR, Request sẽ được gởi đến Handler. Handler sẽ xử lý và trả về response như sau

Logic sẽ được xử lý trong Handler. Nếu chúng ta thêm phần xử lý Validation

Việc thêm Validation sẽ dẫn tới vi phạm Single Responsibility. Để giải quyết vấn đề này, MediatR đưa ra khái niệm IPipelineBehavior.

Trong MediatR, IPipelineBehavior hoạt động như một loại pipeline (sort of pipeline).

Thay vì trực tiếp chuyển Request đến Handler, nó chuyển qua một hoặc nhiều middleware - được gọi là Pipeline Behaviors - xử lý Request và Response theo ba bước:

  • Pre - Mọi thứ đến được chuyển đến Pre-processor. Nó có thể xác nhận Request đến, thực hiện một số biến đổi trên nó, v.v.
  • next() - Nếu bước tiền xử lý thành công, bước tiếp theo sẽ là một phương thức gọi tiếp theo trong pipeline , một middleware khác, hoặc chính Handler ở cuối ống dẫn.
  • Post - Đây là bước cuối cùng được thực hiện sau Logic; khi bước next() hoàn tất. Nó có thể thực hiện một số xử lý sau trên Response được trả về, thực hiện một số biến đổi trên nó, ghi log, xử lý error, v.v. Kết quả được trả lại cho máy khách (hoặc bước trước đó trong pipeline ). 

Bạn có thể thêm nhiều Behavior có cùng cấu trúc vào trong pipeline.

MediatR cung cấp interface IPipelineBehavior như sau

public interface IPipelineBehavior<in TRequest, TResponse> where TRequest : notnull
{
    Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken);
}
Giả sử chúng ta implement LoggingBehavior
using MediatR;
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

    public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        // Pre-processing
        _logger.LogInformation($"Handling {typeof(TRequest).Name}");

        var response = await next();

        // Post-processing
        _logger.LogInformation($"Handled {typeof(TResponse).Name}");

        return response;
    }
}
Đăng ký Dependency Injection
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,> ));

Validation

Giả sử chúng ta có đoạn code thêm Author như sau:

public record AddAuthorCommand: IRequest<int>
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}
public class CreateAuthorHandler : IRequestHandler<AddAuthorCommand, int>
{
    private readonly IAuthorRepository _authorRepository;
    public CreateAuthorHandler(IAuthorRepository authorRepository)
    {
        _authorRepository = authorRepository;
    }
    public Task<int> Handle(AddAuthorCommand request, CancellationToken cancellationToken)
    {
        var result = _authorRepository.AddAuthor(new Services.DataModels.AddAuthorModel
        {
            FirstName = request.FirstName,
            LastName = request.LastName
        });
        return Task.FromResult(result);
    }
}
Chúng ta sẽ thêm đoạn Validation như sau
public Task<int> Handle(AddAuthorCommand request, CancellationToken cancellationToken)
{
    if(string.IsNullOrEmpty(request.FirstName) || string.IsNullOrEmpty(request.LastName))
    {
        return Task.FromResult(0);
    }
    var result = _authorRepository.AddAuthor(new Services.DataModels.AddAuthorModel
    {
        FirstName = request.FirstName,
        LastName = request.LastName
    });
    return Task.FromResult(result);
}

Tách phần Validation Logic

Cài đặt Fluent Validation package

dotnet add package FluentValidation
dotnet add package FluentValidation.AspNetCore
Tách phần validation ra thành class AddAuthorCommandValidator
public class AddAuthorCommandValidator: AbstractValidator<AddAuthorCommand>
{
    public AddAuthorCommandValidator()
    {
        RuleFor(t=> t.FirstName).NotEmpty().WithMessage("The First Name identifier can't be empty.");
        RuleFor(t => t.LastName).NotEmpty().WithMessage("The Last Name identifier can't be empty.");
    }
}
Đăng ký Dependecy Injection cho Validator (ở Program.cs)
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
Cập nhật lại Handler như sau:
using CqrsSample.Services.Repositories;
using FluentValidation;
using MediatR;

namespace CqrsSample.Application.Commands.Handlers;

public class CreateAuthorHandler : IRequestHandler<AddAuthorCommand, int>
{
    private readonly IAuthorRepository _authorRepository;
    private readonly IValidator<AddAuthorCommand> _validator;

    public CreateAuthorHandler(IAuthorRepository authorRepository, IValidator<AddAuthorCommand> validator)
    {
        _authorRepository = authorRepository;
        _validator = validator;
    }
    public Task<int> Handle(AddAuthorCommand request, CancellationToken cancellationToken)
    {
        //if(string.IsNullOrEmpty(request.FirstName) || string.IsNullOrEmpty(request.LastName))
        //{
        //    return Task.FromResult(0);
        //}
        _validator.ValidateAndThrow(request);
        var result = _authorRepository.AddAuthor(new Services.DataModels.AddAuthorModel
        {
            FirstName = request.FirstName,
            LastName = request.LastName
        });
        return Task.FromResult(result);
    }
}
Thêm API Create Author
[HttpPost]
public async Task<int> Create(AddAuthorModel author)
{
    var newAuthorCommand = new AddAuthorCommand
    {
        FirstName = author.FirstName,
        LastName = author.LastName
    };
    var result = await _mediator.Send(newAuthorCommand);
    return result;
}
Thực hiện request với FirstName hoặc LastName rỗng, kết quả trả về như sau:
FluentValidation.ValidationException: Validation failed: 

 -- FirstName: The First Name identifier can't be empty. Severity: Error...

Khai báo Validation Pipeline

Mục đích là model sẽ được validate trước khi gọi hàm Handler()
public sealed class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : class, IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators) => _validators = validators;

    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        if (!_validators.Any())
        {
            return await next();
        }

        var context = new ValidationContext<TRequest>(request);

        var errors = _validators
                    .Select(x => x.Validate(context))
                    .SelectMany(x => x.Errors)
                    .Select(validationFailure => new ValidationFailure(
                        validationFailure.PropertyName,
                        validationFailure.ErrorMessage))
                .ToList();

        if (errors.Any())
        {
            throw new ValidationException(errors);
        }

        return await next();
    }
}
Giải thích

Tất cả các model có kiểu TRequest sẽ được inject vào Pipeline. Ở đây chúng ta sử dụng interface IValidator do abstract class AbstractValidator kế thừa từ nó

public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators) => _validators = validators;
//------------
public abstract class AbstractValidator<T> : IValidator<T>, IEnumerable<IValidationRule> {
	internal TrackingCollection<IValidationRuleInternal<T>> Rules { get; } = new();
	private Func<CascadeMode> _classLevelCascadeMode = () => ValidatorOptions.Global.DefaultClassLevelCascadeMode;
	private Func<CascadeMode> _ruleLevelCascadeMode = () => ValidatorOptions.Global.DefaultRuleLevelCascadeMode;

Bạn thực hiện đăng ký bên dưới LoggingBehavior

builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,> ));
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
Hi vọng với những đoạn code nhỏ này sẽ giúp ích bạn trong việc xây dựng API

Tham khảo

https://www.linkedin.com/pulse/advanced-features-mediatr-package-pipeline-behaviors/

https://doumer.me/cqrs-command-validation-with-mediatr-in-asp-net-core/

How to use MediatR Pipeline Behaviours

CQRS Validation with MediatR Pipeline and FluentValidation



Nhận xét

  1. https://medium.com/codenx/mediatr-with-notification-system-use-case-in-asp-net-core-21fce433bb19

    Trả lờiXóa
  2. https://csandunblogs.com/mediatr-notifications/

    Trả lờiXóa

Đăng nhận xét

Bài đăng phổ biến từ blog này

[ASP.NET MVC] Authentication và Authorize

Một trong những vấn đề bảo mật cơ bản nhất là đảm bảo những người dùng hợp lệ truy cập vào hệ thống. ASP.NET đưa ra 2 khái niệm: Authentication và Authorize Authentication xác nhận bạn là ai. Ví dụ: Bạn có thể đăng nhập vào hệ thống bằng username và password hoặc bằng ssh. Authorization xác nhận những gì bạn có thể làm. Ví dụ: Bạn được phép truy cập vào website, đăng thông tin lên diễn đàn nhưng bạn không được phép truy cập vào trang mod và admin.

ASP.NET MVC: Cơ bản về Validation

Validation (chứng thực) là một tính năng quan trọng trong ASP.NET MVC và được phát triển trong một thời gian dài. Validation vắng mặt trong phiên bản đầu tiên của asp.net mvc và thật khó để tích hợp 1 framework validation của một bên thứ 3 vì không có khả năng mở rộng. ASP.NET MVC2 đã hỗ trợ framework validation do Microsoft phát triển, tên là Data Annotations. Và trong phiên bản 3, framework validation đã hỗ trợ tốt hơn việc xác thực phía máy khách, và đây là một xu hướng của việc phát triển ứng dụng web ngày nay.

Tổng hợp một số kiến thức lập trình về Amibroker

Giới thiệu về Amibroker Amibroker theo developer Tomasz Janeczko được xây dựng dựa trên ngôn ngữ C. Vì vậy bộ code Amibroker Formula Language sử dụng có syntax khá tương đồng với C, ví dụ như câu lệnh #include để import hay cách gói các object, hàm trong các block {} và kết thúc câu lệnh bằng dấu “;”. AFL trong Amibroker là ngôn ngữ xử lý mảng (an array processing language). Nó hoạt động dựa trên các mảng (các dòng/vector) số liệu, khá giống với cách hoạt động của spreadsheet trên excel.