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 argumentsType of
request
argument changed toIRequest<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:
MethodInfo.Invoke (src) — type unsafe method invocation via reflection
Delegate.DynamicInvoke (src) — late-bound method invocation
dynamic Invoke (src) — non-statically typed method invocation
delegate Invoke (src) — type-safe encapsulated method invocation
Expression Invoke (src) — constructing and invoking lambda expression via expression trees
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
:
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:
Handler instances don’t have to be injected into their consumers, which makes their constructors slim-fit.
Handlers are completely decoupled from each other and the rest of the system which makes it more maintainable.
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)