ASP.NET Core: Implementing CQRS Pattern
In this post, we will be implementing Command Query Responsibility Segregation (CQRS) pattern. This pattern is used to separate read and write operations for a data store. The advantage of implementing this pattern is to increase performance, scalability, and security.
What is the Command Query Responsibility Segregation (CQRS) pattern?
In the CQRS pattern, the Command refers to a Database command that represents Insert, Update, and Delete Operations. The Query means the Read operations from the Data Store. This means that we are separating Read and Operations separately to provide service access to callers who are interested in performing either Read or Write operations at a time.
Why do we need CQRS Pattern?
Technically the CQRS implementation helps us to create separate services (APIs) for reading data and writing data. This is useful when we want to use the same data store for performing Read/Write operations. If we implement the logic for performing Read/Write operations in the single service then the increase in concurrent Read/Write requests to this single service may delay the executions. Moreover, if we need to perform any updates in logic for read operations e.g. conditional searches or criteria-based searches then it needs the modification in service, and during this time the other write operations will not be accessible. In such situations, it's always better to implement separate services for Read/Write Operations and hence CQRS pattern is useful.
Figure 1 provides an idea of the CQRS pattern
Figure 1: The CQRS Pattern
Implementation of CQRS Pattern
To implement the code for this article you must be aware of the concepts of ASP.NET core. I have already published articles on ASP.NET Core on this and this link. The code for this article is implemented using Visual Studio 2022 Enterprise Edition with .NET 7 SDK and the database created in SQL Server database.
Listing 1 shows the script for creating a database and table
Create Database Company;
Go
Use Company;
Go
CREATE TABLE [dbo].[Department](
[DeptUniqueId] [int] IDENTITY(1,1) NOT NULL,
[DeptNo] [varchar](20) NOT NULL,
[DeptName] [varchar](100) NOT NULL,
[Capacity] [int] NOT NULL,
[Location] [varchar](100) NOT NULL,
PRIMARY KEY CLUSTERED([DeptUniqueId] ASC)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]) ON [PRIMARY]
GO
Listing 1: Database and Table Script
Step 1: Open Visual Studio 2022 and create a new ASP.NET Core API project targetted .NET 7. Name this project Core_API_CQRS.
Step 2: In this project add the following packages so that we can use EntityFrameork Core 7 and build the project
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Relational
Microsoft.EntityFrameworkCore.Design
Microsoft.EntityFrameworkCore.Tools
Step 3: Open the command prompt and navigate to the project folder and run the command shown in Listing 2 to generate entity classes and DbContext class for performing CRUD Operations. We are using the EntityFramework Core Database First approach.
dotnet ef
dbcontext scaffold “Data Source=.;Initial Catalog=Company; Integrated
Security=SSPI; TrustServerCertificate=True”
Microsoft.EntityFrameworkCore.SqlServer -o Models
Listing 2: The command to scaffold DbContext using EntityFramework Core
Once this command is executed successfully, the Models folder will be added to the Project. This folder will have the Department.cs file that contains the Department entity class. The CompanyContext.cs file contains the CopanyContext class that is derived from the DbContext class to manage the mapping with the Department table in the Company database. The CompanyContext class contains OnConfiguring() method with the connection string in it. Copy the connection string from this method and paste it into the appsettings.json file as shown in Listing 3. We will be reading this connection string using the ICofiguration contract.
"ConnectionStrings": {
"AppConnStr": "Data Source=.;Initial Catalog=Company;Integrated
Security=SSPI"
}
Listing 3: The Database Connection String
Step 4: In the project add a new folder and name it CQRSPattern. We will be using this folder to add contract interfaces and their classes for performing Query and Command (Read and Write) operations. In this folder add a new interface file and name it IQueryRepository.cs. In this file add the code as shown in Listing 4.
public interface IDepartmentQueriesRepository
{
Task<IEnumerable<Department>> GetDepartmentsAsync();
Task<Department> GetByDeptNoAsync(int dno);
}
Listing 4: The Query Repository Interface
The interface shown in Listing 4, declares methods for performing Query (read) operations. In the same folder add a new interface file and name it ICommandRepository.cs. In this file, we will add an ICommandRepository interface that contains Command methods (write) as shown in Listing 5
public interface IDepartmentCommandRepository
{
Task<Department> SaveDepartmentAsync(Department dept);
}
Listing 5: The Command methods
Step 5: In the same folder created in Step 4, add a new class file named QueryClass.cs. In this file, we will add DepartmentQueryRepository class. This class will implement IDepartmentQueryRepository interface and will implement all of its methods. These methods will be constructor-injected CompanyContext class so that we can perform Query operations in this class. The code for this class is shown in Listing 6
public class DepartmentQueryRepository : IDepartmentQueriesRepository
{
CompanyContext ctx;
public DepartmentQueryRepository(CompanyContext ctx)
{
this.ctx = ctx;
}
async Task<Department> IDepartmentQueriesRepository.GetByDeptNoAsync(int dno)
{
return await ctx.Departments.FindAsync(dno);
}
async Task<IEnumerable<Department>> IDepartmentQueriesRepository.GetDepartmentsAsync()
{
return await ctx.Departments.ToListAsync();
}
}
Listing 6: The Query class
In the same folder, add a new class file and name it CommandClass.cs. In this class file, we add a DepartmentCommandRepository class. Like DepartmentQueryRepository, the DepartmentCommandRepository is also a constructor injected with CompanyContext class. This class implements IDepartmentCommandRepository interface and implements all its methods so that Command (Write) operations can be performed. The code for the class is shown in Listing 7.
public class DepartmentCommandRepository : IDepartmentCommandRepository
{
CompanyContext ctx;
public DepartmentCommandRepository(BajajCompanyContext ctx)
{
this.ctx = ctx;
}
async Task<Department> IDepartmentCommandRepository.SaveDepartmentAsync(Department dept)
{
var result = await ctx.Departments.AddAsync(dept);
await ctx.SaveChangesAsync();
return result.Entity;
}
}
Listing 7: The DepartmentCommandRepository class
Step 6: We will register DbContext, Command, and Query classes in Dependency Container as shown in Listing 8
// Add services to the container.
builder.Services.AddDbContext<BajajCompanyContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("AppConnStr"));
});
builder.Services.AddControllers()
.AddJsonOptions(options => {
options.JsonSerializerOptions.PropertyNamingPolicy = null;
});
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddScoped<IDepartmentCommandRepository, DepartmentCommandRepository>();
builder.Services.AddScoped<IDepartmentQueriesRepository, DepartmentQueryRepository>();
Listing 8: Dependency Registration
Now we have created two separate classes for Command and Query operations, we will add two separate API controllers to the Controllers folder of the project.
Step 7: In the Controllers folder, add a new Empty API Controller and name it DepartmentQueryController. In this controller, we will inject the IDepartmentQueriesRepository using constructor injection. This controller defines two HttpGet methods to perform Query operations as shown in Listing 9
[Route("api/[controller]")]
[ApiController]
public class DepartentQueryController : ControllerBase
{
IDepartmentQueriesRepository _queryRepo;
public DepartentQueryController( IDepartmentQueriesRepository queryRepo)
{
_queryRepo = queryRepo;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetAsync(int id)
{
var response = await _queryRepo.GetByDeptNoAsync(id);
return Ok(response);
}
[HttpGet]
public async Task<IActionResult> GetAsync()
{
var response = await _queryRepo.GetDepartmentsAsync();
return Ok(response);
}
}
Listing 9: The Query Controller
In the Controllers folder add a new Empty API Controller and name it DepartmentCommandController. In this controller, we will inject IDepartmentCommandRepsitory using constructor injection. In this controller, we will add the HttpPost method to perform Command operations as shown in Listing 10
[Route("api/[controller]")]
[ApiController]
public class DepartmentCommandController : ControllerBase
{
private readonly IDepartmentCommandRepository _commandRepo;
public DepartmentCommandController(IDepartmentCommandRepository repository)
{
_commandRepo = repository;
}
[HttpPost]
public async Task<IActionResult> PostAsync(Department dept)
{
var response = await _commandRepo.SaveDepartmentAsync(dept);
return Ok(response);
}
}
Listing 10:The Command Controller
Here we have added two separate controllers for performing Query and Command operations. The advantage of this approach is that based on the requirements for performing Read and Write operations end-users can call that specific controller. This increases the execution performance of the API and further says that if we want to add further logic for ReadWrite operations then that specific controller can be changed.
Now we can run the project and test these APIs. The code for this article can be downloaded from this link.
Conclusion: When we plan to apply any pattern for implementing data operations using APIs or Microservices then the use of the CQRS pattern is recommended.