Avalist

ใช้ MediatR ใน .NET Core ทำ Validation Handling

ในตัวอย่างเราจะลองมาใช้ FluentValidation ในการทำ Validation และใช้ MediatR ในการจัดการ Validation ใน .NET Core แบบง่ายๆ โดยที่เราไม่ต้องเขียน validation เองให้ยุ่งยาก
ใช้ MediatR ใน .NET Core ทำ Validation Handling

ก่อนอื่นถ้าใครยังไม่รู้จัก MediatR หรือว่ายังไม่เคยอ่านบทความก่อนหน้าลองเข้าไปดูรายละเอียดของ MediatR ในบทความ “มาทำความรู้จัก MediatR ใน .NET Core” กันก่อนนะครับ

ในบทความนี้จะยกตัวอย่างงานที่ผมใช้จริงๆ ในการทำ validation ใน .NET Core โดยที่เราจะใช้ FluentValidation ในการทำ Validation แต่จะมีการปรับเปลี่ยนนิดหน่อยโดยจะนำตัว MediatR มาใช้ในการจัดการ Validation ใน .NET Core แบบง่ายๆ

ตัวอย่างโปรแกรมจะเป็นตัวเดียวกับที่เราสร้าง API โดยใช้ “Dotnet API Command and Query pattern” แต่ทีนี้เราจะเพิ่มเติมการทำ validation ใน request ของเรา

Go to Avalist’s GitHub repo
ตัวอย่างโปรแกรม

เริ่มต้นทำ Validation ด้วย FluentValidation

เริ่มต้นด้วยการเพิ่ม package ของ FluentValidation ลงในโปรเจคของเรา

	   
dotnet add package FluentValidation
dotnet add package FluentValidation.DependencyInjectionExtensions

ในโปรแกรมที่เราสร้างไว้ก่อนหน้าจะมีการสร้าง Product โดยมี request ดังนี้ ทีนี้เราจะทำการเพิ่ม class สำหรับ validation ด้วย FluentValidation ขึ้นมาหนึ่งตัวที่ชื่อ CreateProductCommandValidator

	   
  ├── Applications
  │   ├── Features
  │       ├── Products
  │           ├── CreateProduct
  │               ├── CreateProductCommand.cs
  │               ├── CreateProductCommandHandler.cs
  │               └── CreateProductCommandValidator.cs

ในตัว validation เราจะมีการกำหนดเงื่อนไขของข้อมูลที่ต้องการ validate ดังนี้

	   
public class CreateProductCommandValidator : AbstractValidator<CreateProductCommand>
{
    public CreateProductCommandValidator()
    {
        RuleFor(x => x.Code)
            .NotEmpty()
            .MaximumLength(10);

        RuleFor(x => x.Name)
            .NotEmpty()
            .MaximumLength(100);

        RuleFor(x => x.Description)
            .MaximumLength(500);

        RuleFor(x => x.Price)
            .GreaterThan(0);

        RuleFor(x => x.UnitInStock)
            .GreaterThanOrEqualTo(0);
    }
}

Registering FluentValidation ใน ASP.NET Core

เราสามารถ register ตัว Fluent Validator ที่เราเพิ่งสร้างขึ้นมาได้ตามตัวอย่างด้านล่างนี้

	   
services.AddScoped<IValidator<CreateProductCommand>, CreateProductCommandValidator>

หรือถ้าหากเรามี validator หลายตัวและไม่อยากที่จะทำการ register ทีละตัวเราสามารถทำการ register ทีละ assembly ได้ดังนี้

	   
services.AddValidatorsFromAssembly(applicationAssembly);

วิธีการเรียกใช้งาน FluentValidation

เราสามารถเรียกใช้งาน FluentValidation ได้ง่ายๆ ดังนี้

	   
var validationResult = await validator.ValidateAsync(request, cancellationToken);
if (!validationResult.IsValid)
{
    return Results.ValidationProblem(validationResult.ToDictionary());
}

แต่ในบทความเราจะลองมาใช้ MediatR ในการจัดการ Validation ใน .NET Core โดยที่เราไม่ต้องเขียน validation เองให้ยุ่งยาก ในตัวอย่างคือเราจะมีการทำ Pipeline ใน MediatR ที่จะทำการ validate request ทุกครั้งที่มีการส่ง request มา ถ้าเรา validate แล้วติด error เราก็จะทำการ return response กลับไปว่า error ตามรูปแบบที่เรากำหนดไว้

	   
public sealed class ValidationPipelineBehavior<TRequest, TResponse>(
    IEnumerable<IValidator<TRequest>> validators)
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : class, ICommand<TResponse>
{
    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        ValidationFailure[] validationFailures = await ValidateAsync(request);

        if (validationFailures.Length == 0)
        {
            return await next();
        }
        else
        {
            throw new FluentValidation.ValidationException(validationFailures);
        }
    }

    private async Task<ValidationFailure[]> ValidateAsync(TRequest request)
    {
        if (!validators.Any())
        {
            return [];
        }

        var context = new ValidationContext<TRequest>(request);
        ValidationResult[] validationResults = await Task.WhenAll(
            validators.Select(validator => validator.ValidateAsync(context)));

        ValidationFailure[] validationFailures = validationResults
            .Where(result => !result.IsValid)
            .SelectMany(result => result.Errors)
            .ToArray();

        return validationFailures;
    }
}

หลังจากสร้าง Pipeline เสร็จแล้วเราก็ register ตัว Pipeline ที่เราสร้างไว้ใน Startup.cs ดังนี้

	   
services.AddMediatR(config =>
{
    config.RegisterServicesFromAssembly(applicationAssembly);
    config.AddOpenBehavior(typeof(ValidationPipelineBehavior<,>));
});

ถัดไปก็เป็นการ Handling Validation Exception ที่เราเพิ่งสร้างขึ้นมา

หลังจากนั้นเราจะสร้าง ExceptionHandler ที่จะทำการ handle exception ที่เกิดขึ้นใน Pipeline ของ MediatR โดยที่เราจะสร้าง ExceptionHandler ที่ implement จาก IExceptionHandler ดังนี้ โดยวิธีการนี้จะเป็นการดักจับ error ทั้งหมดที่เกิดขึ้นในระบบและทำการ return กลับไปให้ client ในรูปแบบที่เรากำหนดไว้

	   
public class GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
    {
        logger.LogError(exception, "An exception occurred: {Message}", exception.Message);

        (var httpStatusCode, var errors) = GetHttpStatusCodeAndErrors(exception);
        httpContext.Response.ContentType = "application/json";
        httpContext.Response.StatusCode = (int)httpStatusCode;
        var serializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
        var response = JsonSerializer.Serialize(errors, serializerOptions);

        await httpContext.Response.WriteAsync(response, cancellationToken).ConfigureAwait(false);
        return true;
    }

    private static (HttpStatusCode httpStatusCode, IReadOnlyCollection<CustomError>) GetHttpStatusCodeAndErrors(Exception exception) =>
            exception switch
            {
                FluentValidation.ValidationException req => (HttpStatusCode.BadRequest, CustomError.ValidationError(req.Errors)),
                _ => (HttpStatusCode.InternalServerError, new[] { CustomError.GeneralServerError })
            };
}

ในตัวอย่างเราจะ return custom type ที่ชื่อว่า CustomError ที่เราสร้างขึ้นมาเอง โดยที่เราสามารถกำหนด error code และ message ได้ตามต้องการ

	   
public class CustomError(string code, string message)
{
    public string Code { get; set; } = code;

    public string Message { get; set; } = message;

    public static CustomError[] ValidationError(IEnumerable<FluentValidation.Results.ValidationFailure> failures) => CreateErrors(failures).ToArray();

    public static CustomError GeneralServerError => new("ServerError", "Internal Server Error");

    private static IEnumerable<CustomError> CreateErrors(IEnumerable<FluentValidation.Results.ValidationFailure> failures)
    {
        foreach (var error in failures)
        {
            yield return new CustomError(error.ErrorCode, error.ErrorMessage);
        }
    }
}

หลังจากนั้นเราก็ register ตัว ExceptionHandler ที่เราสร้างไว้ใน Startup.cs ดังนี้

	   
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
.
.
app.UseExceptionHandler();

ทดสอบการทำงาน

หลังจากลองทดสอบการทำงานของโปรแกรมเราจะได้ผลลัพธ์ดังนี้

Fluent Validation with MediatR

Fluent Validation with MediatR

ในตัวอย่างทั้งหมดเป็นแค่วิธีการทำ validation handling โดยการประยุกต์ใช้กับ MediatR เผื่อเป็นทางเลือกให้กับ developer สาย .NET จะได้ลองใช้เทคนิคนี้ในการทำ Validation ในโปรเจคของตัวเองได้

ถ้าหากใครสนใจอยากดูวิธีการใช้ MediatR กับเทคนิคอื่นๆ ลองดูได้ในบทความที่เขียนไว้ในนี้ได้นะครับ

  1. ใช้ MediatR ในการรับส่ง request/response ของตัว API “Dotnet API Command and Query pattern”
  2. ใช้ MediatR ในการทำ caching response ของตัว API “มาใช้ MediatR ทำ Response Caching”
  3. ใช้ MediatR ในการ log request/response ของตัว API “มาใช้ MediatR ทำ Logging”
  4. ใช้ MediatR ในการทำ Validation Handling “มาใช้ MediatR ทำ Validation handling”

อ่านบทความเพิ่มเติม

ใช้ MediatR ทำ Response Caching Caching น่าจะเป็นสิ่งที่จำเป็นหลักของการออกแบบโปรแกรมสมัยใหม่ เพื่อรองรับกับการจำนวน request ที่เพิ่มขึ้นอย่างรวดเร็ว ในบทความนี้เราจะมาลองทำ caching โดยใช้ MediatR กัน
4 min read
ใช้ MediatR ใน .NET Core ทำ Logging MediatR เป็นไลบรารีใน .NET Core ที่ใช้เพื่อจัดการการส่งข้อความหรือคำสั่งระหว่างระหว่าง component หลักการทำงานของ MediatR จะอยู่บนพื้นฐานของ Mediator Design Pattern โดยการประสานการทำงานระหว่างส่วนต่างๆ
1 min read
มาทำความรู้จัก MediatR ใน .NET Core MediatR เป็นไลบรารีใน .NET Core ที่ใช้เพื่อจัดการการส่งข้อความหรือคำสั่งระหว่างระหว่าง component หลักการทำงานของ MediatR จะอยู่บนพื้นฐานของ Mediator Design Pattern โดยการประสานการทำงานระหว่างส่วนต่างๆ
1 min read