Janki Mehta
Posted on October 23, 2023
CQRS stands for Command Query Responsibility Segregation. It's an architectural pattern that separates read operations (queries) from write operations (commands) in an application. Implementing CQRS results in two separate models - one optimized for writes and another for reads.
MediatR is a popular library that helps implement the CQRS pattern in .NET applications. In this post, we will see how to use MediatR to achieve CQRS segregation in an ASP.NET Core web application.
Overview of CQRS
In a traditional CRUD-based application, the same model is used for queries (reads) as well as commands (writes). This can lead to some issues as the model has to cater to conflicting requirements of operations.
For example, for high-performance reads, we want to denormalize the data and cache it in a way that makes rendering fast. But for writes, we want to normalize the data to maintain data integrity.
CQRS solves this by separating the two operations into separate models. Some key advantages of CQRS include:
- Models can be optimized independently for read and write operations.
- Separation of concerns - code is cleaner and maintainable.
- Queries never modify data, leading to conflict-free reads.
- Easy caching of query models for performance.
- Writing logic is simplified without worrying about effects on reads.
- Parallel development of read and write stacks is possible.
- Better scalability as the load can be distributed.
Of course, CQRS adds the complexity of having to maintain separate models. There are also consistency issues to handle. Extra infrastructure is required to sync the two models.
Despite the cons, the CQRS pattern is gaining popularity due to the needs of modern applications. The complexity is managed by using frameworks like MediatR.
Implementing CQRS with MediatR
MediatR acts as an in-process mediator that handles the routing of requests and responses in an application. Here is how it helps implement CQRS:
- Requests: User actions like button clicks are modeled as request objects. These represent write/command operations.
- Notifications: After processing a request, a notification event is raised. Handlers listen for notifications to update read models.
- Queries: Query requests represent read operations. The query handler returns data from the read model. This separation is achieved by having requests, notifications, and queries inherited from marker-base classes defined in MediatR.
Let's see how to apply this when building an ASP.NET Core application.
Building the ASP.NET Core Application
We will build a simple contact management web application to illustrate CQRS with MediatR. It will have pages to view, add, and edit contacts. Under the hood, we will implement separate models for the commands and queries.
Installing NuGet Packages
Create an ASP.NET Core Web Application and install the following NuGet packages:
MediatR
MediatR.Extensions.Microsoft.DependencyInjection
MediatR provides the core library, while the extensions package adds the necessary DI setup.
Defining Requests
Requests encapsulate the write operations. For our app, we can define requests for add, update, and delete operations on a contact:
public class CreateContactRequest : IRequest<int>
{
public string Name {get; set; }
public string Email {get; set; }
}
public class UpdateContactRequest : IRequest
{
public int Id {get; set; }
public string Name {get; set; }
public string Email {get; set; }
}
public class DeleteContactRequest : IRequest
{
public int Id { get; set; }
}
The request classes contain the payload required for each operation.
Handling Requests
Handlers will perform the actual data modification required by requests. For example:
public class CreateContactHandler : IRequestHandler<CreateContactRequest, int>
{
private Repository repo;
public CreateContactHandler(Repository repo)
{
this.repo = repo;
}
public Task<int> Handle(CreateContactRequest request, CancellationToken cancellationToken)
{
var contact = new Contact();
contact.Name = request.Name;
contact.Email = request.Email;
repo.Create(contact);
return contact.Id;
}
}
The handler takes the request, creates a domain model object, passes it for persistence, and returns the new ID. Similar handlers are created for update and delete requests.
Raising Notifications
After the data is modified, notifications are raised so query models can be updated.
public class ContactCreatedNotification : INotification
{
public Contact Contact { get; set; }
}
// In Create handler
var contact = // create contact
repo.Create(contact)
_mediator.Publish(new ContactCreatedNotification{ Contact = contact});
The event payload has the new data. Publish method of MediatR sends it to handlers.
Query Model
The read model is simplified and denormalized for fast fetching:
public class ContactSummary
{
public int Id {get; set;}
public string Name {get; set;}
public string Email {get; set;}
}
The query handler returns data from this model:
public class GetContactsQueryHandler : IRequestHandler<GetContactsQuery, List<ContactSummary>>
{
private readonly Repository repo;
public GetContactsQueryHandler(Repository repo)
{
this.repo = repo;
}
public Task<List<ContactSummary>> Handle(GetContactsQuery request, CancellationToken cancellationToken)
{
return repo.GetContactSummaryList();
}
}
Syncing Read Model
The read model is updated by handling notifications.
public class ContactCreatedNotificationHandler : INotificationHandler<ContactCreatedNotification>
{
private readonly Repository repo;
public ContactCreatedNotificationHandler(Repository repo)
{
this.repo = repo;
}
public Task Handle(ContactCreatedNotification notification, CancellationToken cancellationToken)
{
var summary = new ContactSummary
{
Id = notification.Contact.Id,
Name = notification.Contact.Name,
Email = notification.Contact.Email
};
repo.AddSummary(summary);
return Task.CompletedTask;
}
}
Similar handlers update the summary on other notifications like update and delete.
That completes the write and read implementations. Next, we wire up MediatR in the ASP.NET Core application.
Integrating MediatR
In Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddMediatR(typeof(Startup)); // Registers handlers and mediatr pipeline
// ... other services
}
The AddMediatR call registers the handlers and mediator classes.
To dispatch a request:
public class HomeController : Controller
{
private readonly IMediator _mediator;
public HomeController(IMediator mediator)
{
_mediator = mediator;
}
public async Task<IActionResult> CreateContact()
{
var result = await _mediator.Send(new CreateContactRequest());
return View();
}
}
Similarly, queries can be dispatched from controllers and page code behind files.
Final Words
Implementing CQRS with MediatR allows us to reap the benefits of segregated models in an efficient manner. The framework abstracts the majority of complexity.
Some further enhancements to make the implementation production-ready:
- Asynchronous handlers for scale
- Caching for query performance
- Use multiple databases for physical separation
- Event sourcing to maintain an audit trail CQRS is particularly useful for heavy-traffic applications like e-commerce. The structured separation of concerns makes the system easier to scale and optimize.
I hope this gives you a good overview of implementing domain-driven design patterns like CQRS in ASP.NET apps using MediatR. Let me know if you have any other questions!
Posted on October 23, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.