Command handler pattern

There are several ways to handle commands in CQRS. One established pattern is to use command handlers. Command handler is a class that has one method to handle a single command type. For example, we might have a command handler like this:

public class CreateClassifiedAdHandler : 
IHandleCommand<Contracts.ClassifiedAds.V1.Create>
{
private readonly IEntityStore _store;

public CreateClassifiedAdHandler(IEntityStore store)
=> _store = store;

public Task Handle(Contracts.ClassifiedAds.V1.Create command)
{
var classifiedAd = new ClassifiedAd(
new ClassifiedAdId(command.Id),
new UserId(command.OwnerId));

return _store.Save(classifiedAd);
}
}

Two interfaces are used by the command handler above. These interfaces look like this:

public interface IHandleCommand<in T>
{
Task Handle(T command);
}

public interface IEntityStore
{
Task<T> Load<T>(string id);
Task Save<T>(T entity);
}

Bear in mind that the IEntityStore interface is simplified and not all persistence methods can be represented by such interface.

We then can use this command handler in the API:

using System.Threading.Tasks;
using Marketplace.Contracts;
using Microsoft.AspNetCore.Mvc;

namespace Marketplace.Api
{
[Route("/ad")]
public class ClassifiedAdsCommandsApi : Controller
{
private readonly IHandleCommand<ClassifiedAds.V1.Create>
_createAdCommandHandler;

public ClassifiedAdsCommandsApi(
IHandleCommand<ClassifiedAds.V1.Create> createAdCommandHandler)
{
_createAdCommandHandler = createAdCommandHandler;
}

[HttpPost]
public Task Post(ClassifiedAds.V1.Create request) =>
_createAdCommandHandler.Handle(request);
}
}

You can see here that we reference the command handler utilizing the interface. It gives us some freedom in choosing the implementation we want to use. To start with, we can register the implementation we already have:

services.AddSingleton<IEntityStore, RavenDbEntityStore>();
services.AddScoped<
IHandleCommand<Contracts.ClassifiedAds.V1.Create>,
CreateClassifiedAdHandler>();
Here we register the RavenDbEntityStore class that implements IEntityStore. We aren't going to look at the actual implementation here, but since RavenDb is the document database, such class could be rather trivial to implement.

What we have done so far is very straightforward, but since we are using the IHandleCommand<T> interface in our API, we can do something more interesting. For example, we can create a generic command handler that retries failures:

public class RetryingCommandHandler<T> : IHandleCommand<T>
{
static RetryPolicy _policy = Policy
.Handle<InvalidOperationException>()
.Retry();

private IHandleCommand<T> _next;

public RetryingCommandHandler(IHandleCommand<T> next)
=> _next = next;

public Task Handle(T command)
=> _policy.ExecuteAsync(() => _next.Handle(command));
}

We just need to change the service registration to look like this:

services.AddScoped<IHandleCommand<Contracts.ClassifiedAds.V1.Create>>(c =>
new RetryingCommandHandler<Contracts.ClassifiedAds.V1.Create>(
new CreateClassifiedAdHandler(c.GetService<RavenDbEntityStore>())));

Here we wrap the actual command handler inside the generic retry handler. Since they both implement the same interface, we can build a pipeline using a composition of these classes. We can continue adding more elements to the chain. For example, use stuff like a circuit breaker or a logger.

Hence that we can add more properties to the command class (remember weak schema), but the only handler that we might want to change because of this would be the actual command handler. All transient handlers will remain unchanged because we are using the command type, which is a complex type, as a parameter, so the interface definition itself doesn't change.

The command handler pattern is compelling, and it is adhering to the single responsibility principle (SRP). At the same time, each HTTP method in our API would require a separate command handler as a dependency. It is not a big deal if we have two or three methods, but we might have a little more than that. We might predict having more than ten methods in our Classified Ad API, and an adequate number of command handlers, just by looking at the result of our EventStorming session. Command handlers at least need the entity store as a dependency, and since all WebAPI controllers are instantiated per scope, all command handlers will be instantiated and injected as well, with all their dependencies. It is possible to mitigate the instantiation of a vast dependency tree by using factory delegates instead of dependencies per request, so each method would be able to instantiate its handler:

using System;
using System.Threading.Tasks;
using Marketplace.Contracts;
using Microsoft.AspNetCore.Mvc;

namespace Marketplace.Api
{
[Route("/ad")]
public class ClassifiedAdsCommandsApi : Controller
{
private readonly Func<IHandleCommand<ClassifiedAds.V1.Create>>
_createAdCommandHandlerFactory;

public ClassifiedAdsCommandsApi(
Func<IHandleCommand<ClassifiedAds.V1.Create>> createAdCommandHandlerFactory)
=> _createAdCommandHandlerFactory = createAdCommandHandlerFactory;


[HttpPost]
public Task Post(ClassifiedAds.V1.Create request) =>
_createAdCommandHandlerFactory().Handle(request);
}
}

This approach would require more advanced registration since we aren't using the actual type, but a delegate. Another solution might be to use Lazy<IHandleCommand<T>> as a dependency. Again, it will require more complex registration. The registration challenge might be resolved by using another dependency injection container, like Autofac, which supports automatic factory delegates and Lazy<T> out of the box.

In this book, we will not be using the command handler pattern, but instead, we will implement command handling using the application service. We already started to implement a simple service in the previous section and will continue in the next paragraph. The reason for the command handler detour to exist is to bring a better overview of useful patterns since no patter is good enough for all use cases.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.99.71