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
https://medium.com/codenx/mediatr-with-notification-system-use-case-in-asp-net-core-21fce433bb19
Trả lờiXóahttps://csandunblogs.com/mediatr-notifications/
Trả lờiXóa