洋葱(整洁)架构中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
相关产品推荐
相关产品推荐

