Search code examples
c#.netoopcqrs

How to extract business rule validation in CQRS?


I'm using MediatR and the ErrorOr library to handle responses in a .NET application. I have a command handler that performs several business rule validations, such as checking if a stop exists in the database and if the user has the reviewer role.

Currently, my AddServiceTasksHandler performs these validations inline. As I understand, AbstractValidator is used to validate the Validation rules, but I'm unsure how to handle business rule validation. I'm considering extracting the validation logic into a separate interface, but I'm not certain if this is the best approach.

Here's my current implementation:

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;
    }

How can I refactor this handler to extract the business rule validation logic? Should I create a separate interface like IValidateAddServiceTasks with a Validation method ? what is the best approch

   public IValidateAddServiceTasks
    {
        ErrorOr<bool> Validation()
    }

Solution

  • Much grief originates from a failure to distinguish between validation and business rules. I'd like to suggest the heuristic that validation is self-contained operation, preferably a pure function, whereas a business rule is exactly that: Business logic.

    Entire books have been written about how to best model business logic, or, as it's also often known, a Domain Model. Thus, we can hardly give you a short answer that dictates how you must do it. That's the job of a software architect, and in order to do that job properly, one needs to know the entire context of the code base, including all the non-technical or socio-technical requirements.

    But since validation seems to be a solved problem, you might get benefit out of moving all validation to the beginning of the handler, or (less commonly useful) to the end of it.