DEV Community

Cover image for Command & Query: Domain - Mediator
Darjan Bogdan
Darjan Bogdan

Posted on • Edited on • Originally published at Medium

Command & Query: Domain - Mediator

The story is a part of the series about software architecture powered by command-query separation principle and aspect-oriented programming.


Example powered by requests and handlers

In the preceding chapter, the base model, which represents commands, queries, and their handlers, was explained and implemented as an abstract model in C#. Since the model consists of essentially two interfaces, it’s possible to provide a large variety of implementations making it suitable to model many business domains.

Simple user management will be taken as an example of one. Using the mentioned model as a backbone, many different actions on the user object will be implemented. To keep it clean and simple, only the user’s unique identifier and name are properties in focus. The class itself could be easily expanded or completely changed based on the business domain’s needs.

public class User
{
    public Guid Id { get; set; }
    public string Name { get; set; }
}

To implement the logic for getting a single User using its identifier, it’s necessary to implement two interfaces in separate classes. An instance of one class holds the data needed to perform a task, an instance of another holds the business logic to perform a task.

GetUserQuery class implements IRequest<User> and carries the user identifier needed to get the user successfully. GetUserQueryHandler class, on the other hand, implements IRequestHandler<GetUserQuery, User> interface and has a method that performs the logic to get the user from some source. Simply, the method gets GetUserQuery object, passes UserId to the GetUserFromSource method to fetch the user and returns the retrieved User object. Data fetching and materialization are not in the focus at the moment, so GetUserFromSource could be virtually anything.

Following the example, the other actions on User object could be implemented similarly, using the same pair of generic interfaces or their derivatives such as IRequest and IRequestHandler<TRequest>.

Having all the handler implementations in place, with the help of a dependency injection container, each handler instance can be injected into another class. A typical Web API controller would look something like this:

What immediately comes to attention is the constructor over-injection smell, and as such should be avoided or reduced to a minimum, especially if it would appear regularly.

Mediator — definition and model

Instead of individually injecting instances of closed generic interfaces, a single non-generic interface with a generic method shall be used instead. Implementation of such interface serves as a mediator between any caller and any request handler, effectively decoupling them from each other and completely removing the initial code-smell.

Although IRequestHandlerMediator seems like a reasonable choice to name such interface, I prefer to use IBus only because the base model can be furtherly expanded using various handler decorators to form a pipeline and/or in-process bus.

The initial version of the interface and class implementation could look like this:

As mentioned earlier, Bus implementation serves as a mediator, with the responsibility to invoke Handle method on a correct handler instance. Handler instantiation is usually managed by a dependency injection container, so instanceFactory function delegate is passed into the constructor as the only dependency of the implementation. It simply takes Type argument and returns an instance of that type as an object.

While looking simple and straightforward, to invoke SendAsync method both type parameters must be explicitly defined, which makes the usage inconvenient:

var query = new GetUserQuery();
var user = await bus.SendAsync<GetUserQuery, User>(query, default);

For a reason C#’s type inference doesn’t take return types into account, as a matter of fact, it fails immediately per specification:

If the supplied number of arguments is different than the number of parameters in the method, then inference immediately fails.C# Language Specification, Version 5.0, § 7.5.2 Type Inference

Still, explicitly defined type parameters look noisy, especially when used frequently, with long descriptive type names. Considering that both query and response types are known at compile-time, writing simple clean code with type safety in place should be achievable:

var query = new GetUserQuery();
var user = await bus.SendAsync(query, default);

To make the usage more pleasant, redundant data needs to be avoided meaning the SendAsync method signature needs to be adapted:

  • TRequest type parameter removed — to match the number of method parameters and arguments

  • Type of request argument changed to IRequest<TResponse> — to keep the same type constraint

With the constraints in place, implementation draft looks like the following:

Conforming to type inference rules leads to more complex implementation because the concrete type of request argument is unknown at the compile time. The absence of the type makes it impossible to construct the closed generic handler type which is required to get the correct handler instance via instanceFactory delegate.

There are different ways to solve the problem. For example, in MediatR library internal generic handler wrappers are used to solve the same issue.

In this case, only Delegates and Reflection are used to fill the gap. To have a reasonable solution, a few requirements and limitations must be met:

  • Simple enough to be easily maintainable

  • Performance in-line with existing solutions to be usable

  • No handler instance caching to avoid captive dependencies

For simplicity, a naive implementation is shown, while a more comprehensive and optimized solution with safety checks and error handling in place can be found in Pype repository:

The simplest way to solve the problem is to reintroduce the initially implemented SendAsync<TRequest, TResponse> method with the small, but important difference. The type of request argument is object, not TRequest anymore.

The side-effect of casting back to TRequest is neglectable to the benefits of using closed instance delegate — to achieve fast and type-safe invocation via SendAsyncDelegate<TResponse> delegate. It wouldn’t be possible to use it if the argument’s type remained TRequest.

Mediator — implementation benchmarking

There are many different ways to invoke SendAsync<TRequest, TResponse> function without knowing type parameters at compile time, some are:

Using dedicated delegate type (as shown in the example) which represents the reference to the SendAsync<TRequest, TResponse> method is the most performant one. Based on a simple benchmarking using dummy PingRequest and PingResponse:

Send method execution benchmarks chart

Besides the mentioned approaches, Generic invoke presents the benchmarking of the initial version — the one with inconvenient API. It serves as a reference point to evaluate whether the trade-off between performance and convenience pays off.

Data sets (series) in the diagram are representing:

  • Simple — a naive implementation for different method invocation approaches. As expected, each is slower by order of magnitude in comparison to the direct method invocation. In the case of expression trees, it’s even worse.

  • Optimized — a reasonable implementation using caching where possible to avoid usually expensive interaction with System.Reflection namespace. The instantiation of methods, delegates, expressions or lamdas is completely mitigated by caching, invocation itself is almost the only thing that counts.

Although the best approach is still more than 2 times slower than the reference point, in absolute terms it’s only ~70ns slower which is negligible in most of the cases. Based on that and the benchmarking comparison with the other libraries, it’s safe to conclude that this kind of solution is efficient enough to be used on a daily basis.

To conclude, it’s always nice to see that extra effort to make a more convenient API paid off eventually:

  1. Handler instances don’t have to be injected into their consumers, which makes their constructors slim-fit.

  2. Handlers are completely decoupled from each other and the rest of the system which makes it more maintainable.

  3. Handler invocation through IBus mediator comes with proper type inference support which makes the usage simple and code clean.

Finally, the fun part is reached, with the base model and mediator in place, it’s possible to expand the whole concept with many different cross-cutting aspects which will naturally fit into the whole picture. One of them, data validation, is covered in the next chapter.


All the gists, examples or solutions will be done in C# using the latest .Net Core/Standard versions and hosted on Github.

Top comments (0)