Understanding CQRS Pattern using .NET Core & MediatR

Command and Query Responsibility Segregation with MediatR

Today, we’re goinng to talk about CQRS pattern and how you can implement it using .net core for a Web API. To the people who don’t know what that is, CQRS means Command and Query Responsibility Segregation, big name I know, but at a glance you can understand that is has something to do with segregating resposibility, ish! right? Well let me explan. Implementing CQRS in your application can maximize its performance, scalability, and security. The flexibility created by migrating to CQRS allows a system to better evolve over time and prevents update commands from causing merge conflicts at the domain level.

Why and When? 🤔

First, a little bit of background. In traditional architectures, the same data model is used to query and update a database. That’s simple and works well for basic CRUD operations. In more complex applications, however, this approach can become unwieldy. Another problem is read and write workloads are often asymmetrical, with very different performance and scale requirements.

They are asymmetrical, because,

  • There is often a mismatch between the read and write representations of the data, such as additional columns or properties that must be updated correctly even though they aren’t required as part of an operation.
  • Data contention can occur when operations are performed in parallel on the same set of data.
  • The traditional approach can have a negative effect on performance due to load on the data store and data access layer, and the complexity of queries required to retrieve information.
  • Managing security and permissions can become complex, because each entity is subject to both read and write operations, which might expose data in the wrong context.

The Command Query Responsibility Segregation (CQRS) pattern separates a service’s write(commands) tasks from its read(query) tasks.

CQRS pattern separates a service’s write tasks(commands) from its read tasks(query).

Having separate query and update models simplifies the design and implementation. However, one disadvantage is that CQRS code can’t automatically be generated from a database schema using scaffolding mechanisms such as ORM tools. Read activity tends to be more frequent than writing, thus you can reduce response latency by placing read data sources in strategic geolocations for better performance. Also separating write from read activity leads to more efficient scaling of storage capacity based on real-world usage.

CQRS is ideal for scenarios where (these are the main pain points that CQRS addresses, there and many more use cases),

  • Task-based user interfaces where users are guided through a complex process as a series of steps or with complex domain models. The write model has a full command-processing stack with business logic, input validation, and business validation. The write model may treat a set of associated objects as a single unit for data changes (an aggregate, in DDD terminology) and ensure that these objects are always in a consistent state. The read model has no business logic or validation stack, and just returns a DTO for use in a view model. The read model is eventually consistent with the write model.
  • Scenarios where performance of data reads must be fine-tuned separately from performance of data writes, especially when the number of reads is much greater than the number of writes. In this scenario, you can scale out the read model, but run the write model on just a few instances. A small number of write model instances also helps to minimize the occurrence of merge conflicts.
  • Scenarios where the system is expected to evolve over time and might contain multiple versions of the model, or where business rules change regularly.
High level CQRS implementation. You can either have two separate databases for reads and writes (SQL for writes and NoSQL for reads etc.) or one databases for both operations.

In simple terms, CQRS helps you to,

  • Enforce single-responsibility principle (SRP) which states that every module, class or function in a computer program should have responsibility over a single part of that program’s functionality.
  • Manage frequent changes to your application.
  • Manage read heavy applications by introducing two databases, one for read and one for write.

Hands On! 👨‍💻

Create a dotnet core web API project using Visual Studio or VSCode. Or download the demo project and follow along.

You can get the basic boilerplate code using these commands. Open up an empty folder and execute,

git init
git remote add origin https://github.com/nishanc/CQRSDemoDotNet
git fetch origin 2a48ef328ec5ca1e4db5e8c0a65a6cf918ee6f0f
git reset --hard FETCH_HEAD

Inside CQRSDemo folder, restore packages, and start app using following commands if you’re on .NET CLI, or use Visual Studio.

dotnet restore
dotnet watch run

At this point, you have one controller which have two endpoints to return list of Products and a Product by Id from an SQLite database.

Now we will create a Query (read data) by creating a folder named Queries and inside it, create a new static class GetProduct.cs . A query or a command (write data) has 3 parts to it.

  • The query or command itsef — will be the data we give to the operation either to read or write.
  • Handler — is the business logic we execute on the data
  • Respnse — will return the output

On top of all that, we use records introduced in C#9, to implement queries/commands/responces. We use records because they are immutable, meaning that once they are created they can’t be changed (mutated) afterwards.

And to do this we need another package called MediatR, also install MediatR.Extensions.Microsoft.DependencyInjection as well. MediatR is .NET implementation of mediator pattern, which encapsulates, communication between objects within a mediator object. Objects no longer communicate directly with each other, but instead communicate through the mediator. This reduces the dependencies between communicating objects, thereby reducing coupling. Read the doc 📜.

dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

You can see that a Query is a type of IRequest and the Handler is a type of IRequestHandler both are comming from MediatR. Our query has a parameter, id because we’re trying to get a product by id from this query.

Now let’s improve this Handler class making it talk to the database context we have, to get the product by id (commit ✏️).

We have injected the database context and used it to return the response we disired. So now we can call this from our controller, let’s move into that. We’re going to ditch the repository alltogether and call the mediator to send the query. Also add MediatR to the dependancy container by adding

services.AddMediatR(typeof(Startup).Assembly);

to the ConfigureServices method in Startup.cs

You can see that I have injected the mediator to the controller and removed the repository (commit ✏️). The mediator.Send() returns a Response that we created in the Query earlier.

Now if you startup the application, you should be able to get the 1st Product, if you pass id as 1.

Now let’s add a Command by following the same pattern. Add a new folder called Commands, inside it a new class AddProduct

We’re using a Data Transfer Object class ProductToAddDto.cs to send data back and forth, it encapsulates only the data we need from the domain class.

public class ProductToAddDto {
public string ProductName { get; set; }
public string Description { get; set; }
}

Then in the Handler we use the entity framework just like before to save the data. We don’t have a Response this time because this is a command; not something that you return a response, but you can if you want to.

And in the controller, new endpoint (commit ✏️).

Run the application and try to a new Product.

Ok so now that you understand what is CQRS and how wire things up, let’s dive deep.

In MediatR you can build your own pipelines directly inside MediatR, so you don’t have add any dependencies using using decorators. It's a more natural way to enhance your handlers with behavior and better supported in containers.

Let’s take a simple use case. Assume you want to log requests being executed via MediatR. Let’s try to implement it using MediatR pipeline behaviors. Lets’s create a new class LoggingBehavior in Helpers folder, taken straight from the doc 📜. (commit ✏️)

Register it in Startup.cs (commit ✏️)

services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));

We saw earlier that Requests are handled by RequestHandlers, which implement the IRequestHandler<TRequest, TResponse> interface, where TRequest is the request object and TResponse is the return value.

But a pipeline behavior is an implementation of IPipelineBehavior<TRequest, TResponse>. It represents a similar pattern to filters in ASP.NET MVC/Web API. Before each request, all pipeline behaviours are called, if there are any, which wrap requests.

Now if you run your application, send few requests and look at the terminal you will see that our Logger is logging requests (types).

Nice! 🥳, let’s do some changes to the LoggingBehavior and see if we can get more information out from it. First we’ll get the execution time for each request. (commit ✏️)

Will change this a little bit again to log the request data comming in (commit ✏️).

Amazing right? but one thing to note is that pipeline behaviors are only compatible with IRequestHandler<TRequest,TResponse> and can't be used with INotificationHandler<TRequest>.(INotificationHandler<> is something you can use to handle all notifications (doc 📜))

Similarly you can use pipeline behavoirs to handle exceptions as well (doc 📜).

I hope this explanation is clear and verbose enough for you to get started with mediator/MediatR. Leave some thoghts about this, or questions are also welcome. Stay safe! 🖖

Refer:

Systems Design • Social Innovation • Cloud • ML