Reputation: 244
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.
IPipelineBehaviour
HTTP Request
(Ex: Create User would send the command to a CreateUserCommandHandler
, which would take care of all the work)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'
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 handlerTask<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.
Upvotes: 1
Views: 2545
Reputation: 392
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;
}
Upvotes: 3