Nested mediatR calls #595
Replies: 1 comment 1 reply
-
Hi @ayuksekkaya, Thanks for the comments and a great question. In the article linked by your StackOverflow post Jimmy Bogard points out that while using nested MediatR calls can add complexity and that he is not a fan, there’s no definitive rule against it. It depends on understanding and managing the trade-offs. While I see why some would say that commands should not send other commands, encapsulating all data changes within commands ensures single responsibility. This way, it’s clear where and how something like user creation is being handled. Moving this logic outside the I favor consistency, and I find that most CRUD operations can be created following this pattern:
Consistently trying to follow this makes the codebase very easy to extend and reason about. It might sound complicated, but the code below shows it's very simple. [PublicAPI]
public sealed record CreateTenantCommand(TenantId Id, string OwnerEmail, bool EmailConfirmed)
: ICommand, IRequest<Result<TenantId>>;
public sealed class CreateTenantValidator : AbstractValidator<CreateTenantCommand>
{
public CreateTenantValidator()
{
RuleFor(x => x.OwnerEmail).NotEmpty().SetValidator(new SharedValidations.Email());
}
}
public sealed class CreateTenantHandler(ITenantRepository tenantRepository, IMediator mediator, ITelemetryEventsCollector events)
: IRequestHandler<CreateTenantCommand, Result<TenantId>>
{
public async Task<Result<TenantId>> Handle(CreateTenantCommand command, CancellationToken cancellationToken)
{
if(await tenantRepository.ExistsAsync(command.Id, cancellationToken))
{
return Result<TenantId>.Conflict("Tenant already exists.");
}
var tenant = Tenant.Create(command.Id, command.OwnerEmail);
await tenantRepository.AddAsync(tenant, cancellationToken);
events.CollectEvent(new TenantCreated(tenant.Id, tenant.State));
await mediator.Send(new CreateUserCommand(tenant.Id, command.OwnerEmail, UserRole.Owner, command.EmailConfirmed), cancellationToken);
return tenant.Id;
}
} I would say it’s very clear what’s going on here. This way, for example, the It might feel “magical” when the I recently switched PlatformPlatform from Clean Architecture to Vertical Slice Architecture, which has simplified the code. In Clean Architecture, logic was spread across different layers, and I used domain events to handle, for example, the creation of an Owner user when a tenant was created. In Vertical Slice Architecture, it is much easier to create a full vertical slice in one file, and having one command call another makes it very clear what is going on without having to duplicate domain logic or move logic out of a command. I don’t see any mention of concurrency problems in your link, and I don’t see why there should be any. MediatR is very predictable in how it handles events. Regarding the risk of infinite loops mentioned in the article, it’s an issue present in any form of nested calls—commands, services, or otherwise. I don't recall ever facing such a problem in a production system in my 30 years of building production-grade software. All that said, I would strive to avoid multiple nested layers of commands calling commands. No architecture can fully enforce good design choices, and developers must still use sound judgment to avoid bad patterns. |
Beta Was this translation helpful? Give feedback.
-
Hello? thank you for this great work. I am going through the codebas and playing around with the product in general. I see that we are doing some nested mediatR calls. Calling "send" from inside a Handle method. For example in the Tenant Creation command. My understanding was that injecting mediatR through DI to another handler like you are doing can cause concurrency issues and will create a new ef change tracker. Also noted here
https://stackoverflow.com/questions/49042123/is-it-ok-to-have-one-handler-call-another-when-using-mediatr
Is this not the case? If so, should it be refactored?
Beta Was this translation helpful? Give feedback.
All reactions