Coordinating Query And Command Handlers with Mediatr

2k views Asked by At

I am using Mediatr to implement the CQRS pattern in dotnet core 3.0. I had some questions regarding how to coordinate different multiple queries and commands. From what I have read online, here are some best practices that people describe.

  • Query handlers should not depend on command handlers
  • Command handlers should not depend on query handlers
  • Instead of using decorators, use IPipelineBehaviour
  • Each command should map one to one to an HTTP Request (Ex: Create User would send the command to a CreateUserCommandHandler, which would take care of all the work)
  • For command handlers, return void . Using the Mediatr framework, I would return Task<Unit>

Here is my issue, right now, I am implementing IUserStore<TUser> for AspNet.Identity and lets say we're looking at this example

    public async Task<IdentityResult> DeleteAsync(User user, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        if (user == null) return IdentityResult.Failed(new ArgumentNullError(nameof(user)));
        
        bool userExists = await _mediator.Send(new UserExistsQuery {UserId = user.Id}, cancellationToken);
        
        if (!userExists) return IdentityResult.Failed(new EntityDoesNotExistError(user));
        
        await _mediator.Send(new DeleteUserCommand {User = user}, cancellationToken);
        return IdentityResult.Success;
    }

In this method, I am basically using the implementing user store to coordinate different queries and commands in order to delete a user. In this case, I could see doing the following in order to make it thin as possible and more closely follow the 'one command per http request'

  1. Get rid of UserExistsQuery & UserExistsQueryQueryHandler, move that query into the DeleteUserCommandHandler and query using a repository (which UserExistsQueryHandler already does now) rather than being dependent on a query handler
  2. Instead of returning Task<Unit> , return something like an IdentityResult

The reason I am hesitant to do #2 is that it feels like I am returning something on based on the context that the command is being used. I am returning an IdentityResult just because I need it for this one instance.

Furthermore, the reason I had it split out like this in the first place was for re-usability. I wanted to be able to make a number of queries and commands that I could re-use elsewhere. If I return an IdentityResult, then this kind of defeats the purpose as I wouldn't really need this anywhere but in UserStore<TUser>

I've been reading about IPipelineBehaviour but it seems like that is more of a generic solution for all query/command handlers (i.e.: that pipeline could be run for every command/query if the appropriate types exist in your assembly). But could IPipelineBehaviour be used to implement a custom pipeline? In my example, I would move all that logic to a pipeline that would only run for DeleteUserCommand?

I've searched for articles on this subject but couldn't really find anything useful - or maybe I am searching for the wrong terms. My jargon may be wrong, but I could create coordinating services that are only dependent on IMediatr to complete deleting a user. Any feedback and/or reading material would be appreciated.

1

There are 1 answers

0
Mortaza Ghahremani On BEST ANSWER

I think you misunderstand the CQRS concept. The DeleteAsync Operation is a command itself. So if you need to read some data to proceed with your operation, this is not a query, but, just a read operation. So whenever you need to read some data in your command operation you have to get it through your repositories. Be aware that, by using CQRS, you separate paths to user queries and commands not read and writes. In commands, every write and read must pass through your domain model. So, probably there exist a repository in your domain layer to fetch data from your write database. But for queries, there is no need to use the Domain model. so your code must be like below:

public async Task<IdentityResult> DeleteAsync(User user, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        _mediator.Send(new DeleteUserCommand(){});       
    }

And DeleteUserCommand Handler is like:

protected override async Task Handle(DeleteUserCommand request, CancellationToken cancellationToken)
        {
            if (user == null) return IdentityResult.Failed(new ArgumentNullError(nameof(user)));
        
        bool userExists = userRepository.ExistUser(request.UserId);
        
        if (!userExists) return IdentityResult.Failed(new EntityDoesNotExistError(user));
        
        await userRepository.DeleteAsync(request.UserId);
        return IdentityResult.Success;
        }