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

如何在CQRS中提取业务规则验证?.NET下MediatR与ErrorOr的重构方案

MediatR + ErrorOr 业务规则验证重构方案

问题背景

我在.NET应用中使用MediatR和ErrorOr库处理响应,现有AddServiceTasksHandler命令处理器将多项业务规则验证(如检查站点是否存在、用户是否拥有审核员角色等)内联实现。我了解AbstractValidator用于验证规则,但不确定业务规则验证的处理方式,考虑将验证逻辑提取到单独接口(如下方示例),但不确定这是否为最佳方案,希望了解如何重构该处理器以提取业务规则验证逻辑及最佳实现方式:

现有命令与处理器代码:

public record AddServiceTasksCommand(IEnumerable<ServiceTask> ServiceTasks, int DomainId, int StopId) : IRequest<ErrorOr<Created>>;

public class AddServiceTasksHandler : IRequestHandler<AddServiceTasksCommand, ErrorOr<Created>>
{
    private readonly IClaimsAccessor _claimsAccessor;
    private readonly IStopRepository _stopRepository;
    private readonly IMediator _mediator;

    public AddServiceTasksHandler(IClaimsAccessor claimsAccessor, IStopRepository stopRepository, IMediator mediator)
    {
        _claimsAccessor = claimsAccessor ?? throw new ArgumentNullException(nameof(claimsAccessor));
        _stopRepository = stopRepository ?? throw new ArgumentNullException(nameof(stopRepository));
        _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
    }
    public async Task<ErrorOr<Created>> Handle(AddServiceTasksCommand request, CancellationToken cancellationToken)
    {
        var hasAccess = _claimsAccessor.ClaimsPrincipal
            .GetDomainAccesses()
            .Where(x => x.Carrier != null)
            .Select(x => x.Carrier.Value)
            .Distinct()
            .Any(x => x == request.DomainId);

        if (!hasAccess) return Error.Unauthorized(description: $"Unauthorized: The user {_claimsAccessor.ClaimsPrincipal.GetUsername()} does not have access to the domain {request.DomainId} ");
       
        var isReviewed = await _stopRepository.IsReviewedAsync(request.DomainId, request.StopId, cancellationToken).ConfigureAwait(false);

        if (isReviewed.IsError)
        {
            return isReviewed.Errors;
        }

        if (isReviewed.Value)
        {
            return Error.Validation(description: "该站点已审核,无法添加任务");
        }

        var isFinalState = await _stopRepository.IsFinalStateAsync(request.DomainId, request.StopId, cancellationToken).ConfigureAwait(false);

        if (isFinalState.IsError) return isFinalState.Errors;

        var isReviewer = _claimsAccessor.ClaimsPrincipal.IsReviewer();
        if (isFinalState.Value && !isReviewer)
        {
            return Error.Validation(description: "站点处于终态,仅审核员可操作");
        }

        var deliveryStatusOrError = await _stopRepository.GetDeliveryStatusAsync(request.DomainId, request.StopId, cancellationToken).ConfigureAwait(false);

        if (deliveryStatusOrError.IsError) return deliveryStatusOrError.Errors;

        var stopOrError = await _stopRepository.GetStopAsync(request.DomainId,
                request.StopId,
                cancellationToken)
            .ConfigureAwait(false);

        if (stopOrError.IsError) return stopOrError.Errors;
        var stop = stopOrError.Value;
        var errors = new List<Error>();
        if (isReviewer)
        {
            foreach (var serviceTask in request.ServiceTasks)
            {

                var service = new StopTask()
                {
                    BusinessUnitId = deliveryStatusOrError.Value.CompanyId,
                    ClientId = stop.Client.Id,
                    DomainId = stop.DomainId,
                    Invoice = serviceTask.InvoiceNumber,
                    // 其他属性赋值
                };
                var result = await AddServiceAsync(service, deliveryStatusOrError.Value.Status, cancellationToken).ConfigureAwait(false);
                if (result.IsError) errors.AddRange(result.Errors);
            }

            return errors.Any() ? errors : Result.Created;
        }

        var deliveryStatus = deliveryStatusOrError.Value;
        var isDriver = _claimsAccessor.ClaimsPrincipal.IsDriver();
        switch (isDriver)
        {
            case true when deliveryStatus.DeliveryDateTime.Date > DateTime.Now.Date:
                return Error.Validation(description: "配送日期未到,司机无法添加任务");
        }

        foreach (var serviceTask in request.ServiceTasks)
        {
            var service = new StopTask()
            {
                BusinessUnitId = deliveryStatusOrError.Value.CompanyId,
                ClientId = stop.Client.Id,
                DomainId = stop.DomainId,
                Invoice = serviceTask.InvoiceNumber,
                // 其他属性赋值
            };
            var result = await AddServiceAsync(service, deliveryStatusOrError.Value.Status, cancellationToken).ConfigureAwait(false);
            if (result.IsError) errors.AddRange(result.Errors);
        }

        return errors.Any() ? errors : Result.Created;
    }

    private async Task<ErrorOr<Success>> AddServiceAsync(StopTask service, string status, CancellationToken cancellationToken)
    {
        // 原有业务逻辑
        return Result.Success;
    }
}

考虑的接口示例:

public interface IValidateAddServiceTasks
{
    ErrorOr<bool> Validation();
}

重构方案

方案1:提取独立业务验证类(基于接口扩展)

扩展你考虑的接口,使其支持异步和命令上下文,将所有验证逻辑迁移到独立类中:

1. 定义通用命令验证接口

public interface ICommandValidator<TCommand>
{
    Task<ErrorOr<Success>> ValidateAsync(TCommand command, CancellationToken cancellationToken);
}

2. 实现AddServiceTasksCommand专属验证器

public class AddServiceTasksCommandValidator : ICommandValidator<AddServiceTasksCommand>
{
    private readonly IClaimsAccessor _claimsAccessor;
    private readonly IStopRepository _stopRepository;

    public AddServiceTasksCommandValidator(IClaimsAccessor claimsAccessor, IStopRepository stopRepository)
    {
        _claimsAccessor = claimsAccessor;
        _stopRepository = stopRepository;
    }

    public async Task<ErrorOr<Success>> ValidateAsync(AddServiceTasksCommand command, CancellationToken cancellationToken)
    {
        var errors = new List<Error>();

        // 验证用户域权限
        var hasAccess = _claimsAccessor.ClaimsPrincipal
            .GetDomainAccesses()
            .Where(x => x.Carrier != null)
            .Select(x => x.Carrier.Value)
            .Distinct()
            .Any(x => x == command.DomainId);

        if (!hasAccess)
        {
            errors.Add(Error.Unauthorized(description: $"未授权:用户{_claimsAccessor.ClaimsPrincipal.GetUsername()}无域{command.DomainId}的访问权限"));
        }

        // 验证站点是否已审核
        var isReviewed = await _stopRepository.IsReviewedAsync(command.DomainId, command.StopId, cancellationToken).ConfigureAwait(false);
        if (isReviewed.IsError)
        {
            errors.AddRange(isReviewed.Errors);
        }
        else if (isReviewed.Value)
        {
            errors.Add(Error.Validation(description: "该站点已完成审核,无法添加服务任务"));
        }

        // 验证站点终态与用户角色
        var isFinalState = await _stopRepository.IsFinalStateAsync(command.DomainId, command.StopId, cancellationToken).ConfigureAwait(false);
        if (isFinalState.IsError)
        {
            errors.AddRange(isFinalState.Errors);
        }
        else
        {
            var isReviewer = _claimsAccessor.ClaimsPrincipal.IsReviewer();
            if (isFinalState.Value && !isReviewer)
            {
                errors.Add(Error.Validation(description: "站点处于终态,仅审核员可添加服务任务"));
            }
        }

        // 验证配送状态可用性
        var deliveryStatusOrError = await _stopRepository.GetDeliveryStatusAsync(command.DomainId, command.StopId, cancellationToken).ConfigureAwait(false);
        if (deliveryStatusOrError.IsError)
        {
            errors.AddRange(deliveryStatusOrError.Errors);
        }

        // 验证站点存在性
        var stopOrError = await _stopRepository.GetStopAsync(command.DomainId, command.StopId, cancellationToken).ConfigureAwait(false);
        if (stopOrError.IsError)
        {
            errors.AddRange(stopOrError.Errors);
        }

        // 验证司机角色与配送时间(非审核员场景)
        if (!_claimsAccessor.ClaimsPrincipal.IsReviewer() && deliveryStatusOrError.IsSuccess)
        {
            var isDriver = _claimsAccessor.ClaimsPrincipal.IsDriver();
            if (isDriver && deliveryStatusOrError.Value.DeliveryDateTime.Date > DateTime.Now.Date)
            {
                errors.Add(Error.Validation(description: "配送日期未到,司机无法添加服务任务"));
            }
        }

        return errors.Any() ? ErrorOr.Fail(errors) : ErrorOr.Success();
    }
}

3. 简化原处理器逻辑

public class AddServiceTasksHandler : IRequestHandler<AddServiceTasksCommand, ErrorOr<Created>>
{
    private readonly IClaimsAccessor _claimsAccessor;
    private readonly IStopRepository _stopRepository;
    private readonly IMediator _mediator;
    private readonly ICommandValidator<AddServiceTasksCommand> _validator;

    public AddServiceTasksHandler(IClaimsAccessor claimsAccessor, IStopRepository stopRepository, IMediator mediator, ICommandValidator<AddServiceTasksCommand> validator)
    {
        _claimsAccessor = claimsAccessor ?? throw new ArgumentNullException(nameof(claimsAccessor));
        _stopRepository = stopRepository ?? throw new ArgumentNullException(nameof(stopRepository));
        _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
        _validator = validator ?? throw new ArgumentNullException(nameof(validator));
    }

    public async Task<ErrorOr<Created>> Handle(AddServiceTasksCommand request, CancellationToken cancellationToken)
    {
        // 先执行所有业务规则验证
        var validationResult = await _validator.ValidateAsync(request, cancellationToken).ConfigureAwait(false);
        if (validationResult.IsError)
        {
            return validationResult.Errors;
        }

        // 验证通过后,仅保留核心业务逻辑
        var deliveryStatusOrError = await _stopRepository.GetDeliveryStatusAsync(request.DomainId, request.StopId, cancellationToken).ConfigureAwait(false);
        var stopOrError = await _stopRepository.GetStopAsync(request.DomainId, request.StopId, cancellationToken).ConfigureAwait(false);
        var stop = stopOrError.Value;
        var errors = new List<Error>();
        var isReviewer = _claimsAccessor.ClaimsPrincipal.IsReviewer();

        foreach (var serviceTask in request.ServiceTasks)
        {
            var service = new StopTask()
            {
                BusinessUnitId = deliveryStatusOrError.Value.CompanyId,
                ClientId = stop.Client.Id,
                DomainId = stop.DomainId,
                Invoice = serviceTask.InvoiceNumber,
                // 其他属性赋值
            };

            var status = deliveryStatusOrError.Value.Status;
            var result = await AddServiceAsync(service, status, cancellationToken).ConfigureAwait(false);
            if (result.IsError) errors.AddRange(result.Errors);
        }

        return errors.Any() ? ErrorOr.Fail(errors) : ErrorOr.Created;
    }

    private async Task<ErrorOr<Success>> AddServiceAsync(StopTask service, string status, CancellationToken cancellationToken)
    {
        // 原有业务逻辑
        return Result.Success;
    }
}

方案2:结合MediatR管道行为实现自动验证

如果希望所有命令统一执行验证逻辑,可以通过MediatR的管道行为自动拦截命令并触发验证:

1. 实现通用验证管道

public class CommandValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
    where TResponse : IErrorOr
{
    private readonly IEnumerable<ICommandValidator<TRequest>> _validators;

    public CommandValidationBehavior(IEnumerable<ICommandValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        var errors = new List<Error>();

        foreach (var validator in _validators)
        {
            var validationResult = await validator.ValidateAsync(request, cancellationToken).ConfigureAwait(false);
            if (validationResult.IsError)
            {
                errors.AddRange(validationResult.Errors);
            }
        }

        if (errors.Any())
        {
            // 利用ErrorOr的静态方法返回错误集合
            return (TResponse)typeof(TResponse).GetMethod("Fail")!.Invoke(null, new object[] { errors })!;
        }

        return await next().ConfigureAwait(false);
    }
}

2. 注册管道与验证器

在DI容器中注册管道行为和验证器(如Program.cs):

builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CommandValidationBehavior<,>));
builder.Services.AddTransient<ICommandValidator<AddServiceTasksCommand>, AddServiceTasksCommandValidator>();

此时处理器无需手动调用验证,管道会自动拦截命令并执行验证逻辑。

方案3:区分输入验证与业务规则验证

如果需要将输入格式验证(如参数非空、数值范围)与业务规则验证(如权限、状态检查)分离,可以用FluentValidation处理输入验证,业务规则用上述验证类实现:

1. 输入格式验证器

public class AddServiceTasksInputValidator : AbstractValidator<AddServiceTasksCommand>
{
    public AddServiceTasksInputValidator()
    {
        RuleFor(x => x.DomainId).GreaterThan(0).WithMessage("域ID必须大于0");
        RuleFor(x => x.StopId).GreaterThan(0).WithMessage("站点ID必须大于0");
        RuleFor(x => x.ServiceTasks).NotEmpty().WithMessage("服务任务列表不能为空");
        RuleForEach(x => x.ServiceTasks).ChildRules(task =>
        {
            task.RuleFor(t => t.InvoiceNumber).NotEmpty().WithMessage("发票号不能为空");
            // 其他输入格式规则
        });
    }
}

2. 集成FluentValidation到MediatR

通过NuGet安装FluentValidation.DependencyInjectionExtensions,然后注册验证器:

builder.Services.AddValidatorsFromAssemblyContaining<AddServiceTasksInputValidator>();
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));

此时输入验证会自动执行,业务规则验证仍用ICommandValidator实现,两者各司其职。


关键注意事项

  • 业务规则验证通常依赖仓储、身份信息等外部服务,不适合放在纯输入验证的FluentValidator中
  • 验证类需遵循单一职责,每个验证器仅负责对应命令的规则检查
  • 复用通用验证逻辑(如用户域权限检查)时,可提取为独立方法或类,在多个验证器中调用
  • 始终用ErrorOr统一返回错误格式,保持响应一致性

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 19:03:10