You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

洋葱(整洁)架构中FluentValidation不生效及状态码优化问题

洋葱架构下FluentValidation验证问题排查与优化

初始问题:验证规则不生效

在洋葱(整洁)架构中使用FluentValidation时,为AddVehicleCommand定义的验证规则未生效,请求会直接进入仓储层,验证逻辑未起到拦截作用。

相关代码实现

验证器定义

public class AddVehicleCommandValidator : AbstractValidator<AddVehicleCommand>
{
    private readonly IVehicleRepositoryAsync _repositoryVehicle;
    public AddVehicleCommandValidator(IVehicleRepositoryAsync repositoryVehicle)
    {
        _repositoryVehicle = repositoryVehicle;

        RuleFor(x => x.Vehicle.Model).NotNull().WithMessage("Model is required.");
        RuleFor(x => x.Vehicle.Maker).NotNull().WithMessage("Maker is required.");
        RuleFor(x => x.Vehicle.UniqueId).NotNull().WithMessage("UniqueId is required").Must(CheckKeyComposition).WithMessage("UniqueId needs to be in format C<number>");
        RuleFor(x => x.Vehicle.UniqueId).MustAsync((x, cancellation) => AlreadyInUse(x)).WithMessage("This id is already in use.");
    }

    private async Task<bool> AlreadyInUse(string key)
    {
        var entity = await _repositoryVehicle.GetById(key);
        if(entity == null)
        {
            return true;
        }
        var id = entity.UniqueId;

        if(key == id)
        {
            return true;
        }
        return false;
    }
   private bool CheckKeyComposition(string key)
    {
        var firstChar = key.Substring(0);
        var secondChar = key.Substring(1, key.Length-1);

        int number;
        bool res = int.TryParse(secondChar, out number);

        if (firstChar.Equals("C") && res)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
}

初始ValidationBehavior实现

public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
         where TRequest : 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())
        {
            var context = new ValidationContext<TRequest>(request);
            var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
            var failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToList();
        }
        return await next();
    }
}

初始依赖注入注册

public static class ServiceExtension
{
    public static void AddApplicationLayer(this IServiceCollection services)
    {
        services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
        services.AddMediatR(Assembly.GetExecutingAssembly());
        services.AddAutoMapper(Assembly.GetExecutingAssembly());

    }
}

AddVehicleCommand及处理逻辑

public class AddVehicleCommand : IRequest<Result<VehicleDto>>
{
    public VehicleDto? Vehicle { get; set; }
}
public class AddVehicleCommandHanlder : IRequestHandler<AddVehicleCommand, Result<VehicleDto>>
{
    private readonly IVehicleRepositoryAsync _vehicleRepository;
    private IMapper _mapper;
    public AddVehicleCommandHanlder(IVehicleRepositoryAsync vehicleRepository, IMapper mapper)
    {
        _vehicleRepository = vehicleRepository;
        _mapper = mapper;
    }

    public async Task<Result<VehicleDto>> Handle(AddVehicleCommand request, CancellationToken cancellationToken)
    {
        Result<VehicleDto> result = new();
        try
        {
            if (request.Vehicle != null)
            {
                VehicleDto vehicle = new();

                vehicle.Maker = request.Vehicle.Maker;
                vehicle.Model = request.Vehicle.Model;
                vehicle.UniqueId = await getNewId();

                Vehicle entity = _mapper.Map<Vehicle>(vehicle);
                var response = await _vehicleRepository.AddAsync(entity);

                result.Success = true;
                result.StatusCode = System.Net.HttpStatusCode.OK;
                result.Data = vehicle;

                return result;
            }
        }
        catch (Exception ex)
        {
            result.ErrorMessage = ex.Message;
            result.StatusCode = System.Net.HttpStatusCode.InternalServerError;
            result.Success = false;
            return result;
        }

        result.ErrorMessage = "Bad request.";
        result.StatusCode = System.Net.HttpStatusCode.BadRequest;
        result.Success = false;
        return result;
    }

    private async Task<string> getNewId()
    {
        var latestId = await getLatestFreeId();
        var newId = (Convert.ToInt32(latestId.Substring(1, latestId.Length-1)) + 1).ToString();

        return string.Format("C{0}", newId);
    }

    private async Task<string> getLatestFreeId()
    {
        var latestId = await _vehicleRepository.GetLatestFreeId();
        if (string.IsNullOrEmpty(latestId))
        {
            //default Vehicle Id
            return "C0";
        }

        return latestId;
    }
}

初始问题原因

验证逻辑已触发但未拦截错误,是因为ValidationBehavior仅收集了验证失败结果,未对失败情况做任何拦截处理,直接调用next()进入命令处理流程。


更新后:验证错误抛出但状态码及格式不符合需求

修改Behavior和注册代码后,验证错误已能抛出,但返回状态码为500,需要改为400 BadRequest,且错误需以列表形式展示。

修改后的ValidationBehavior

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

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

    public Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        var context = new ValidationContext<TRequest>(request);
        var failures = _validators
            .Select(v => v.Validate(context))
            .SelectMany(result => result.Errors)
            .Where(f => f != null)
            .ToList();

        if (failures.Count != 0)
        {
            throw new FluentValidation.ValidationException(failures);
        }

        return next();
    }
}

修改后的依赖注入注册

public static IServiceCollection AddApplicationLayer(this IServiceCollection services)
{
    services.AddMediatR(Assembly.GetExecutingAssembly());
    services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
    services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
    services.AddAutoMapper(Assembly.GetExecutingAssembly());
    return services;
}

解决方案:自定义异常处理与错误格式优化

1. 全局异常捕获中间件(ASP.NET Core)

添加全局异常处理中间件,捕获ValidationException并返回400状态码和格式化的错误列表:

public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlingMiddleware> _logger;

    public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (ValidationException ex)
        {
            _logger.LogError(ex, "Validation failed");
            await HandleValidationExceptionAsync(httpContext, ex);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unhandled exception occurred");
            await HandleExceptionAsync(httpContext, ex);
        }
    }

    private static Task HandleValidationExceptionAsync(HttpContext context, ValidationException ex)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;

        var errors = ex.Errors.Select(e => new { Field = e.PropertyName, Message = e.ErrorMessage });
        var response = new
        {
            StatusCode = context.Response.StatusCode,
            Message = "Validation failed",
            Errors = errors
        };

        return context.Response.WriteAsync(JsonSerializer.Serialize(response));
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

        var response = new
        {
            StatusCode = context.Response.StatusCode,
            Message = "An unexpected error occurred",
            Detail = ex.Message
        };

        return context.Response.WriteAsync(JsonSerializer.Serialize(response));
    }
}

在Program.cs中注册中间件:

app.UseMiddleware<ExceptionHandlingMiddleware>();

2. 直接返回自定义失败结果(无需抛出异常)

如果使用自定义Result<T>类型,可在ValidationBehavior中直接构造失败结果返回,避免抛出异常:

public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
    var context = new ValidationContext<TRequest>(request);
    var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
    var failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToList();

    if (failures.Any())
    {
        var errorMessages = failures.Select(f => f.ErrorMessage).ToList();
        var result = new Result<VehicleDto>
        {
            Success = false,
            StatusCode = System.Net.HttpStatusCode.BadRequest,
            ErrorMessages = errorMessages // 需为Result类添加ErrorMessages列表属性
        };
        return (TResponse)(object)result;
    }

    return await next();
}

3. 优化Result类支持错误列表

更新自定义Result<T>类型,添加错误列表属性以支持多错误返回:

public class Result<T>
{
    public bool Success { get; set; }
    public HttpStatusCode StatusCode { get; set; }
    public T? Data { get; set; }
    public string? ErrorMessage { get; set; }
    public List<string>? ErrorMessages { get; set; }
}

内容的提问来源于stack exchange,提问作者lonelydev101

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 15:50:22