Implementing CQRS using ASP.NET core 5.0
The Command and Query Responsibility Segregation (CQRS)
CQRS pattern separates the read and write operations from the its data model. In conventional architectures, the same data model is being used to update and read transactions. This approach functioning well with basic CRUD operations but the models can become unmanageable when the application gets more complex.
For example when querying from datastore, due to the different returned result set, there will be many DTOs introduced and can be very difficult to manage. When performing update in the database, application may contain complex validations and business logic so the data model may ended up with plethora of implementations with heavy objects (not simplified).
Traditional architecture
In CQRS, a separate request/response model is used for each read and write operation and that is a simplified data transfer model with minimum required fields.
CQRS pattern
Let’s now have a quick look at Pros and Cons of using CQRS.
Pros
Single Responsibility — For commands and queries different object models are being consumed. Hence, each model is responsible for only one and single task
Scalability and Maintainability — With CQRS, each service can be implemented separately and possible to host in different servers. That provides the facility to manage them independently.
Suitable for large and complex applications — CQRS allows to define command and queries to granular level, so that minimize the merge conflicts and possible to work in different segment of the application parallelly.
High Performance — Since the object models/services are scaled out individually, each instance could be fine tuned accordingly.
Cons
Complexity — Too much of round trips and lines of code is involved for even a small query function.
Data Consistency — When CQRS is used with different databases for read and write operations, the read data may be stale. Therefore, the read model must be updated with the write model changes.
Let’s first create a SQL table as follows.
CREATE TABLE [dbo].[PersonalDetails](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Title] [nvarchar](20) NULL,
[Name] [varchar](50) NULL,
[City] [varchar](100) NULL,
[DateOfBirth] [date] NULL,
[LoyaltyPoints] [int] NULL,
CONSTRAINT [PK_CustomerDetails] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
ON [PRIMARY]
) ON [PRIMARY]
GO
Then we will create an ASP.NET Core Web Api project.
Next step is to add the entity model in models folder. Models>PersonalDetails.cs
public class PersonalDetails
{
public int Id { get; set; }
public string Title { get; set; }
public string Name { get; set; }
public string City { get; set; }
public int LoyaltyPoints { get; set; }
}
PersonalDetails.cs
Create DB context class inside Data folder.
Data>CustomerDbContext.cs
using CustomerManagement.Models;
using Microsoft.EntityFrameworkCore;
namespace CustomerManagement.Data
{
public class CustomerDbContext : DbContext
{
public CustomerDbContext(DbContextOptions<CustomerDbContext>
options) : base(options)
{ }
public DbSet<PersonalDetails> PersonalDetails { get; set; }
protected override void OnModelCreating(ModelBuilder
modelBuilder)
{
modelBuilder.Entity<PersonalDetails>
(p=>p.HasKey(_=>_.Id));
}
}
}
CustomerDbContext.cs
make sure to install Microsoft.EntityFrameworkCore and Microsoft.EntityFrameworkCore.SqlServer via NuGet package manager or package manager console.
Install-Package Microsoft.EntityFrameworkCore -Version 5.0.5
Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 5.0.5
Add the connection string in the appsettings.json
appsettings.json
Then in the Startup.cs modify the code as following.
Startup.cs
| Create Command Handler
To save a customer’s personal details, first we have to create a command request model class and this should contain only the required properties. In our scenario, we are using all the properties except ‘Id’ which is anyway auto generated field.
RequestModels>CommandRequests>SaveCustomerRequestModel.cs
namespace CustomerManagement.RequestModels.CommandRequests
{
public class SaveCustomerRequestModel
{
public string Title { get; set; }
public string Name { get; set; }
public string City { get; set; }
public int LoyaltyPoints { get; set; }
}
}
SaveCustomerRequestModel.cs
Let’s introduce an asynchronous abstract method to save customer as follows.
Contracts>CommandHandlers>ISaveCustomerCommandHandler.cs
namespace CustomerManagement.Contracts.CommandHandlers
{
public interface ISaveCustomerCommandHandler
{
Task<int> SaveAsync(SaveCustomerRequestModel
saveCustomerRequestModel);
}
}
ISaveCustomerCommandHandler.cs
Next, we will implement the ‘SaveAsync’ method in the CommandHandler class.
Handlers>CommandHandlers>SaveCustomerCommandHandler.cs
using CustomerManagement.Contracts.CommandHandlers;
using CustomerManagement.Data;
using CustomerManagement.Models;
using CustomerManagement.RequestModels.CommandRequests;
using System.Threading.Tasks;
namespace CustomerManagement.Handlers.CommandHandlers
{
public class SaveCustomerCommandHandler :
ISaveCustomerCommandHandler
{
private readonly CustomerDbContext context;
public SaveCustomerCommandHandler(CustomerDbContext context)
{
this.context=context;
}
public async Task<int> SaveAsync(SaveCustomerRequestModel
saveCustomerRequestModel)
{
var newCustomer = new PersonalDetails
{
Name = saveCustomerRequestModel.Name,
Title = saveCustomerRequestModel.Title,
City = saveCustomerRequestModel.City,
LoyaltyPoints = saveCustomerRequestModel.LoyaltyPoints
};
this.context.PersonalDetails.Add(newCustomer);
await this.context.SaveChangesAsync();
return newCustomer.Id;
}
}
}
SaveCustomerCommandHandler.cs
CustomerDbContext is injected via dependency injection to the command handler and we need to register CommandHandlers in the Startup class.
Startup.cs
Then we will add an end point to save customer details.
Controllers>CustomerController.cs
namespace CustomerManagement.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class CustomerController : ControllerBase
{
private readonly ISaveCustomerCommandHandler
saveCustomerCommandHandler;
public CustomerController(ISaveCustomerCommandHandler
saveCustomerCommandHandler)
{
this.saveCustomerCommandHandler = saveCustomerCommandHandler;
}
[HttpPost]
[Route("save-customer")]
public async Task<IActionResult>
SaveCustomerAsync(SaveCustomerRequestModel requestModel)
{
try
{
var result = await
this.saveCustomerCommandHandler.SaveAsync(requestModel);
return Ok(result);
}
catch
{
throw;
}
}
}
}
CustomerController.cs
Let’s verify the changes using postman.
Postman
| Create Query Handler
We will implement a query model to retrieve all customer information from the server. We do not need to have a request model since there is no parameters are being used.
ResponseModels>QueryResponses>AllCustomerQueryResponseModel.cs
namespace CustomerManagement.ResponseModels.QueryResponses
{
public class AllCustomerQueryResponseModel
{
public int Id { get; set; }
public string Title { get; set; }
public string Name { get; set; }
public string City { get; set; }
public int LoyaltyPoints { get; set; }
}
}
AllCustomerQueryResponseModel.cs
Then the abstract method in
Contracts>QueryHandler>IAllCustomerQueryHandler.cs
namespace CustomerManagement.Contracts.QueryHandlers
{
public interface IAllCustomerQueryHandler
{
Task<List<AllCustomerQueryResponseModel>> GetAllAsync();
}
}
IAllCustomerQueryHandler.cs
Next, we will implement the abstract method.
Handlers>QueryHandler>AllCustomerQueryHandler.cs
namespace CustomerManagement.Handlers.QueryHandler
{
public class AllCustomerQueryHandler : IAllCustomerQueryHandler
{
private readonly CustomerDbContext context;
public AllCustomerQueryHandler(CustomerDbContext context)
{
this.context = context;
}
public async Task<List<AllCustomerQueryResponseModel>>
GetAllAsync()
{
return await this.context.PersonalDetails.Select(s=>new
AllCustomerQueryResponseModel
{
Id=s.Id,
Name=$"{s.Title}.{s.Name }",
City=s.City,LoyaltyPoints=s.LoyaltyPoints
}).ToListAsync();
}
}
}
AllCustomerQueryHandler.cs
Then let’s register the query handler with DI in the Startup.cs
QueryHandler- Startup.cs
It’s the final step and we will add an end point in CustomerController.
[HttpGet]
[Route("get-all")]
public async Task<List<AllCustomerQueryResponseModel>>
GetAllCustomerAsync()
{
try
{
return await this.allCustomerQueryHandler.GetAllAsync();
}
catch
{
throw;
}
}
GetAllCustomerAsync
Do not forget to inject IAllCustomerQueryHandler via the constructor.
CustomerController.cs
Following is the postman result.
get-all customers Postman
Next we will have a query response model with a parameter. The steps are almost same as retrieving all the customers except using Query Request Model. Since you are familiar with the steps already I will quickly show the steps.
RequestModels>QueryRequests>CustomerIdQueryRequestModel.cs
namespace CustomerManagement.RequestModels.QueryRequests
{
public class CustomerIdQueryRequestModel
{
public int CustomerId { get; set; }
}
}
CustomerIdQueryRequestModel.cs
Contracts>QueryHandlers>ICustomerIdQueryHandler.cs
namespace CustomerManagement.Contracts.QueryHandlers
{
public interface ICustomerIdQueryHandler
{
Task<CustomerIdQueryResponseModel>
GetCustomerAsync(CustomerIdQueryRequestModel requestModel);
}
}
ICustomerQueryHandler.cs
Handlers>QueryHandler>CustomerIdQueryHandler.cs
namespace CustomerManagement.Handlers.QueryHandler
{
public class CustomerIdQueryHandler : ICustomerIdQueryHandler
{
private readonly CustomerDbContext context;
public CustomerIdQueryHandler(CustomerDbContext context)
{
this.context = context;
}
public async Task<CustomerIdQueryResponseModel> GetCustomerAsync(CustomerIdQueryRequestModel requestModel)
{
var result = await this.context.PersonalDetails.Where(p = >
p.Id == requestModel.CustomerId)
.FirstOrDefaultAsync();
if (result!=null)
{
return new Customer IdQueryResponseModel
{
CustomerId = result.Id,
Name=$"{result.Title}.{result.Name}",
City = result.City
};
}
return null;
}
}
}
CustomerIdQueryHandler.cs
in Startup.cs
Startup.cs
in CustomerController.cs
[HttpGet]
[Route("customer-id")] [ProducesResponseType(typeof(CustomerIdQueryResponseModel),StatusCodes.Status200OK)]
public async Task<IActionResult> GetCustomerAsync([FromQuery] CustomerIdQueryRequestModel model)
{
try
{
var result = await
this.customerIdQueryHandler.GetCustomerAsync(model);
if (result!=null)
{
return Ok(result);
}
return NotFound($"Customer Id {model.CustomerId} does not exists
!!");
}
catch
{
throw;
}
}
CustomerController.cs
and finally the postman result
Postman
If you had notice the constructor of the CustomerController, everytime a new command or query handler introduced, that has to be injected via constructor and it has be register in the Startup.cs too. Imagine if you had 15 different endpoints with distinct command and query models.
As a solution to this, let’s implement MediatR with CQRS pattern which is again one of the most common implementations of CQRS.
Source: Medium
The Tech Platform
Comments