DEV Community

Cover image for Command & Query: Domain - Abstract Model
Darjan Bogdan
Darjan Bogdan

Posted on • Edited on • Originally published at Medium

Command & Query: Domain - Abstract Model

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


Abstract model in software — where, when and why?

Software problems which share common characteristics could be represented and associated with an abstract model, as a backbone for building generic, reusable solutions. It definitely is challenging to discover such abstractions as a basis to build a usable software model — it requires a lot of effort, time and experience.

Abstraction is the amplification of the essential and the elimination of the irrelevant. — Robert C. Martin, Agile Principles, Patterns, and Practices in C#

There are many fields in mathematics which are typically a good foundation to form an abstract model and finally build a generic solution for a particular or more general problem space. As an example, graph theory is usually the basis for any valid graph representation, structure or model in software. Validity comes from the fact that theoretical definitions are embedded in abstract software models to guard a defined set of laws and provide a predictable interface to a consumer. Ultimately, a well-implemented model can serve as a generic solution for the same problems in the technical domains belonging to different business domains, such as data storage, social networking or communication networks.

Another, more advanced example is the influence of the category theory in functional programming. The theory itself focuses on identification and formalization of a category as a mathematical structure. A category is a graph which represents abstractions of other mathematical concepts and consists of objects (nodes) and their morphisms (arrows). Even though the theoretical concept is quite broad and abstract, some functional programming languages, like Haskell, took advantage of such high-level abstractions, embedded their rules into core language features, and adopted terminology.

How will individual abstraction be truly accomplished in the context of programming language or particular framework is solely up to abstract models’ implementation characteristics. Since source code implementation is opinionated, it’s not surprising that theoretical model realization could vary a lot. It is influenced by many things — to give a brief idea, some are:

  • Language paradigm — object-oriented, functional, event-driven …

  • Language features — type system, generics, pattern matching …

  • Framework features — concurrency, collections, garbage collection …

Determining the right implementation direction is quite challenging, but most importantly it’s rewarding since it’ll make a certain theoretical concept easy to apply and intuitive to use in every-day development.

To support the statement, a good example would be the async/await feature’s advancement in C#. Asynchrony, as a theoretical concept is a well known and hot topic in computer programming. However, every language or framework which supports async programming has own way of achieving it. In other words, each has a different way to implement and expose a meaningful and intuitive API for the developer. In fact, C#/.NET supports 3 different approaches.

It is known that asynchronous programming in .NET gained tremendous popularity once async/await was incorporated into C#, powered by the already existing TPL. The reason is that developers eventually managed to form a software model based on the complex theoretical concept, with intuitive API — to use seamlessly within the context of language and framework.

Just remember how adventurous, hard to maintain and error-prone was asynchronous programming in C# without first-class compiler support, or even before, without first-class library support. Pursuing “asynchronous all the way” principle using earlier patterns (models) was much more complex and rare due to the lack of intuitive API.

It’s fair to mention that such a powerful generalization comes with the cost, and definitely, in essentially complex domains it might be needed to “fallback” to the lower-level models which usually expose a certain degree of implementation details to expose necessary extensibility and configurability (e.g. TaskFactory.StartNew, Task.Run methods, BeginX/EndX pattern).

In conclusion, it was quite challenging to accomplish the current state of the first-class async support in .NET, since it was gradually developed over the years. At the same time, it was rewarding because of the widespread acceptance of the “new/old” async feature in the .NET realm. Besides, it became a de facto standard when dealing with asynchronous domains like HTTP servers.

Command and Query — definition and model

Unlike the complexity of async domain space, CQS principle can be quite simply, yet powerfully modeled to provide a succinct, meaningful and intuitive API to work with.

To begin with the definition, Command is a method that performs an action which mutates the domain’s state or has any kind of side-effect. In addition to that, it doesn’t return anything. On the other hand, Query is a method that performs an action which queries the domain’s state, doesn’t produce side-effects and returns data.

In both cases, there is a given set of rules (characteristics) which any method should adhere to, in order to be classified as one or the other. Moreover, there is a possibility to define the base abstract model which would guard the rules and embed characteristics into the source code, providing a necessary tool in hands to start taking the real advantage of a theoretical principle.

Fair to say, it’s not mandatory to build a software library which supports the concept to start applying it daily. In many cases, even following the rules would be beneficial.

But, having a reusable library built to support a certain principle gives additional room for improvements and a natural upgrade of the whole concept. As described in the introductory article, commands, queries, and their handlers could be represented using only a few generic interfaces, but they can serve as a base ground for the architecture which effortlessly supports building the software components following SOLID principles and AOP paradigm.

Contrary to the mentioned abstract model which includes separate interface pairs for commands and queries, I find it more useful to unify them into a single pair. One interface to represent both (command and query), commonly called request, and another to represent the corresponding request handler.

Umm...I have a question?...Meme

I’m not the greatest fan of the term either. Firstly, it deviates from the terminology coined by CQS principle. Secondly, due to the possible association with the completely different concepts in other domains (e.g. HTTP request). Nevertheless, I finally came to terms with it by simply moving the name distinction to the classes which implement before-mentioned interfaces (e.g. CreateUserCommand : IRequest<TResult> and CreateUserCommandHandler : IRequestHandler<CreateUserCommand, TResult>). After all, the whole abstraction is about commands, queries and their distinction — it’s worth to incorporate it somewhere.

So, the main goal is to have only two base interfaces on top of which other elements will be built, something like:

interface IRequest<TResult> 
{
}

interface IRequestHandler<TRequest, TResult> 
    where TRequest : IRequest<TResult>
{
    Task<TResult> HandleAsync(TRequest request, CancellationToken ct);
}

One could immediately say that this abstract model violates, not only the terminology but also the rules (command has a return value!) defined by the abstract principle — and one would be right. However, I’m completely fine with that deviation, I’m seeing it as a reasonable theoretical model approximation caused by several (technical) limitations:

  • async/await feature requires a method to return Task/Task<T> objects to be awaitableasync void Handle(){} isn’t awaitable and exceptions can’t be handled in the same execution context.

  • void (System.Void) can’t be used as a generic type parameter nor can be passed to a method — Task<void> isn’t allowed.

  • Variance is supported by generic interface and generic delegate types only — class HandlerDecorator<in TRequest, out TResult> isn’t valid.

Despite some limitations, it’s still quite important to support asynchronous programming using widely adopted TAP pattern. That effectively means that interface methods must return Task and/or Task<T>.

Although, it seems that Task HandleAsync(T command) method would be appropriate to use for commands in asynchronous domains, it still involves violation of the principle rules. The method returns an instance of a Task type, which holds the information about the method’s execution (status, exception, identifier…) and doesn’t make it truly fire-and-forget anymore. Furthermore, it would hurt reusability because it’d still be needed to have two separate interfaces for command and query handlers.

To keep the possibility to represent a method which doesn’t “return” anything when having Task<T> as a return type, the special Unit structure must be implemented. That type indicates the absence of a specific value and often is a built-in type in functional languages, like F#.

Having that in place, it’s straightforward to implement any handler which represents command or query, using the same IRequestHandler<TRequest, TResult> interface:

The main benefit of the singular approach lies in the possibility to reuse the same (cross-cutting) components in both cases, considering that commands and queries could share the same characteristics. Like the way of validating or authorizing them before execution, or maybe the way of preserving audit trails, or rate-limiting, and so on.

Consider the following example, to implement a generic validation component for handlers, it is just necessary to build a decorator around a single IRequestHandler<TRequest, TResult> interface. Otherwise, it requires practically two of them, one for ICommandHandler<TCommand> and another for IQueryHandler<TQuery, TResult>.

Similarly, any other aspect could be implemented (e.g. authorization, caching, …) as a single place in the code which handles the particular cross-cutting matter.

It’s not only possible to chain or compose multiple decorators to technically form a request pipeline, but also to conditionally apply them (generic type constraints, attributes), and automatically resolve them using inversion of control techniques.

Result — definition and model

The last thing which I find important to address and (optionally) incorporate into a base model is exception handling or plainly error data propagation.

While it’s not mandatory to have it in place, I think it is important, especially in cases when structured error data (e.g. validation failure reasons) need to be communicated back to an API consumer.

Due to the fact that it’s not possible to have more than one return type in method signatures in C#, and because there is no first-class support for discriminated unions, the easiest and the most straightforward way is to use and extend built-in exception types. So, if there is a need to return some heterogeneous data from a method, one part can be simply propagated within (custom) exception types and just thrown from a method.

Still, structured error data should be treated as part of a domain, and as such should not be included inside a regular exception handling process. Besides, it’s not recommended to throw exceptions in non-exceptional scenarios (e.g. wrong input, token expiry), it makes error handling vexing since exceptions must be handled all the time.

It’s possible to avoid using custom Exception as a carrier of the domain-related data, by “borrowing” and implementing generic Result structure, usually found in functional-first languages.

There are many different ways to implement structure alike in C#, as it is usually based on Church-encoded Either abstraction, which is represented with a single interface method:

interface IEither<TLeft, TRight>
{
    T Match<T>(Func<TLeft, T> left, Func<TRight, T> right);
}

For the sake of clarity and intuitiveness, instead of using highly abstract Either term, Result will be used. The name more closely describes the intent and is more fitting in every-day conversation.

If incorporated as-is into the request handler interface, three generic type parameters must be exposed (i.e. IRequestHandler<TRequest, TResponse, TError>) which adds additional complexity to the model. Also, the whole interface looks quite tedious to work with.

Most of the times, such a degree of model extensibility isn’t required at all and unification of all errors under common type is recommended — there is rarely a need to work with different error types. In that case TError generic type parameter can be omitted and suitable non-generic Error type used instead — making the generic Result class simpler and final version of handler interface cleaner:

interface IRequestHandler<TRequest, TResponse> 
    where TRequest : IRequest<TResponse>
{    
    Task<Result<TResponse>> HandleAsync(
        TRequest request, 
        CancellationToken cancellationToken
    );
}

There are many quality-of-life details which could be added to the abstract model to furtherly enhance every-day usage, such as implicit conversion into Result<T>, set of various helper methods, more derived interfaces, etc.

All these details can be seen in the more comprehensive and complete model shown in Pype repository. The repository will be gradually expanded with the components which will be covered throughout the article series.

Besides that, it’s worth mentioning MediatR and Microbus libraries which are modeling the same principle, both providing similar generic interfaces, but each has some specifics and additional features.

In the next chapter, request handler mediator and request pipeline composition will be covered!


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)