Introduction
Command Query Responsibility Segregation (CQRS) is a design pattern that separates the responsibilities of read and write operations in a software system. This pattern can be particularly useful in large, complex systems that need to handle a high volume of read and write requests.
In this article, we'll explore how to implement CQRS in an ASP.NET 6 web application using Entity Framework Core (EF Core) and MediatR.
Understanding CQRS
In a traditional software system, the same database or data store is used for both read and write operations. This can lead to bottlenecks and performance issues as the volume of requests increases.
CQRS seeks to solve this problem by separating the read and write operations into two separate models. The read model is optimized for querying data, while the write model is optimized for storing and updating data.
This separation of responsibilities allows the system to scale more efficiently, as the read model can be designed to handle a high volume of queries without affecting the write model, and vice versa.
Implementing CQRS in ASP.NET 6
To implement CQRS in an ASP.NET 6 web application, we'll use two libraries: EF Core and MediatR.
EF Core is a lightweight, cross-platform object-relational mapper (ORM) that simplifies the process of working with databases in .NET. It provides a high-level API for executing CRUD (create, read, update, delete) operations against a database.
MediatR is a library that simplifies the process of implementing the CQRS pattern in .NET. It provides a simple, abstracted API for sending and handling commands and queries.
Setting up EF Core
To use EF Core in an ASP.NET 6 web application, you'll need to install the Microsoft.EntityFrameworkCore
NuGet package.
Once the package is installed, you'll need to create a DbContext
class that represents your database. This class will contain the entity classes that map to the tables in your database, as well as any DbSet properties for querying and saving data.
For example, here's a simple DbContext
class for a database with a Customers
table:
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{ }
public DbSet<Customer> Customers { get; set; }
}
Next, you'll need to configure your database connection string in the appsettings.json
file:
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=CQRSExample;Trusted_Connection=True;"
}
}
Finally, you'll need to register your DbContext
class with the ASP.NET 6 dependency injection (DI) container. This can be done in the ConfigureServices
method of the Startup
class:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
}
Setting up MediatR
To install the MediatR library in your ASP.NET 6 project, you'll need to install the MediatR
NuGet package.
Once the package is installed, you'll need to register MediatR with the DI container in the ConfigureServices
method of the Startup
class:
public void ConfigureServices(IServiceCollection services)
{
services.AddMediatR(typeof(Startup));
}
Creating Commands and Queries
In MediatR, a command is a request to perform an action, such as creating or updating a record in the database. A query is a request for information, such as fetching a list of records from the database.
To create a command or query, you'll need to create a request class and a corresponding handler class.
For example, here's a simple command for creating a new customer record:
public class CreateCustomerCommand : IRequest<int>
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
public class CreateCustomerCommandHandler : IRequestHandler<CreateCustomerCommand, int>
{
private readonly AppDbContext _context;
public CreateCustomerCommandHandler(AppDbContext context)
{
_context = context;
}
public async Task<int> Handle(CreateCustomerCommand request, CancellationToken cancellationToken)
{
var customer = new Customer
{
FirstName = request.FirstName,
LastName = request.LastName,
Email = request.Email
};
_context.Customers.Add(customer);
await _context.SaveChangesAsync();
return customer.Id;
}
}
Here, the CreateCustomerCommand
class defines the request data, and the CreateCustomerCommandHandler
class contains the logic for handling the request. The handler class uses EF Core to add the new customer record to the database and save the changes.
Similarly, here's a simple query for fetching a list of customers:
public class GetCustomersQuery : IRequest<List<Customer>>
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class GetCustomersQueryHandler : IRequestHandler<GetCustomersQuery, List<Customer>>
{
private readonly AppDbContext _context;
public GetCustomersQueryHandler(AppDbContext context)
{
_context = context;
}
public async Task<List<Customer>> Handle(GetCustomersQuery request, CancellationToken cancellationToken)
{
var customers = await _context.Customers
.Where(c => c.FirstName.Contains(request.FirstName) && c.LastName.Contains(request.LastName))
.ToListAsync();
return customers;
}
}
Here, the GetCustomersQuery
class defines the request data, and the GetCustomersQueryHandler
class contains the logic for handling the request. The handler class uses EF Core to fetch the list of customers from the database based on the provided first and last names.
Sending Commands and Queries
To send a command or query using MediatR, you'll need to inject an instance of the IMediator
interface into your controller or service class. You can then use the Send
method of the IMediator
instance to send the request and get the response.
For example, here's how you might send the CreateCustomerCommand
in a controller action:
public class CustomersController : Controller
{
private readonly IMediator _mediator;
public CustomersController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> Create(CreateCustomerCommand command)
{
var customerId = await _mediator.Send(command);
return RedirectToAction("Details", new { id = customerId });
}
}
Here, the Create
action uses the Send
method of the IMediator
instance to send the CreateCustomerCommand
request and receive the response (the customer's ID).
Similarly, here's how you might send the GetCustomersQuery
in a service class:
public class CustomersService
{
private readonly IMediator _mediator;
public CustomersService(IMediator mediator)
{
_mediator = mediator;
}
public async Task<List<Customer>> GetCustomers(string firstName, string lastName)
{
var query = new GetCustomersQuery
{
FirstName = firstName,
LastName = lastName
};
return await _mediator.Send(query);
}
}
Here, the GetCustomers
method uses the Send
method of the IMediator
instance to send the GetCustomersQuery
request and receive the response (a list of customers).
Conclusion
CQRS is a powerful design pattern for separating the responsibilities of read and write operations in a software system. By using EF Core and MediatR, it's easy to implement CQRS in an ASP.NET 6 web application and benefit from the improved scalability and performance that it offers.