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()
}
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.