CQRS Pattern C# (How It Works For Developers)

Introduction to CQRS

CQRS stands for Command Query Responsibility Segregation. It's a pattern that focuses on separating the reading of data from its writing. This distinction is crucial for several reasons. First, it allows for more flexible optimization of each operation, improving application performance and scalability. When you separate commands (writes) and queries (reads), you can optimize them independently.

For example, a complex application might require fast read operations but can tolerate slower write operations. By applying CQRS, developers can use different data models for reads and writes, segregating the data access layer to tailor to the specific needs of each operation. In this article, we'll explore the concepts of the CQRS pattern and the IronPDF library.

Core Concepts and Components

The heart of CQRS lies in separating command and query operations, each handling different aspects of data interaction. Understanding these components is crucial for implementing the pattern effectively.

  • Commands: These are responsible for updating data. Commands embody complex business logic and can change the state of data in the data store by acting without returning any information. Commands take the exclusive role of handling write data tasks, directly influencing the state of the application without yielding any output. For instance, adding a new user or updating an existing product details are actions carried out by commands.
  • Queries: Queries, managed by a query handler, retrieve data or data transfer objects without changing the system's state. They are the questions you ask about your data. For example, fetching a user's profile or listing all products available in an inventory are queries. Queries return data but ensure that they do not modify the data or its state.

One of the popular tools for implementing CQRS in .NET applications is MediatR, a mediator pattern library. It helps to reduce the coupling between components of an application, making them communicate indirectly. MediatR facilitates the handling of commands and queries by mediating between the command/query and its handler.

Practical Implementation with ASP.NET Core

Implementing the CQRS pattern in ASP.NET Core involves setting up your project to separate commands and queries, using a library like MediatR to mediate between them. Here's a simplified overview of how you can set up CQRS in your ASP.NET Core application.

Step 1: Set Up Your ASP.NET Application

  1. Start Visual Studio and choose to create a new project.
  2. Search for and select an "ASP.NET Core Web Application" project type. Click Next.

CQRS Pattern C# (How It Works For Developers): Figure 1 - Creating a new ASP.NET project

  1. Give your project a name and set its location. Click Create.
  2. Choose the "Web Application (Model-View-Controller)" template for ASP.NET Core. Ensure you're targeting the .NET Core version that suits your requirements. Click Create.

Step 2

Next, you'll want to organize your project for CQRS. You can do this by adding folders to separate commands, queries, and the common interfaces they'll use. In the Solution Explorer, right-click on your project, go to "Add", then "New Folder". Create three folders: "Commands", "Queries", and "Interfaces".

In the "Interfaces" folder, add interfaces for your commands and queries. For a command, you might have an interface ICommandHandler with a method Handle that takes in a command and performs the action. For a query, you could have an interface IQueryHandler with a method Handle that takes in a query and returns data.

CQRS Pattern C# (How It Works For Developers): Figure 2 - Example of how files could be organised

Step 3

Now, let's add a command and query to demonstrate. Suppose your application manages tasks, and you want to add a task (command) and retrieve tasks (query).

In the "Interfaces" folder, add two interfaces:

//Define interfaces for your handlers:
public interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}
public interface IQueryHandler<TQuery, TResult>
{
    TResult Handle(TQuery query);
}
//Define interfaces for your handlers:
public interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}
public interface IQueryHandler<TQuery, TResult>
{
    TResult Handle(TQuery query);
}
'Define interfaces for your handlers:
Public Interface ICommandHandler(Of TCommand)
	Sub Handle(ByVal command As TCommand)
End Interface
Public Interface IQueryHandler(Of TQuery, TResult)
	Function Handle(ByVal query As TQuery) As TResult
End Interface
VB   C#

In the "Commands" folder, add a class AddItemCommand with properties for the task details. Also, add a class AddItemCommandHandler that implements ICommandHandler and contains the logic to add a task to the database.

In the "Queries" folder, add a class GetTasksQuery that represents a request for tasks. Add another class GetTasksQueryHandler that implements IQueryHandler and contains the logic to retrieve tasks from the database.

For a simple example, your AddItemCommand might look like this:

public class AddItemCommand
{
    public string Name { get; set; }
    public int Quantity { get; set; }
    // Constructor
    public AddItemCommand(string name, int quantity)
    {
        Name = name;
        Quantity = quantity;
    }
}
public class AddItemCommand
{
    public string Name { get; set; }
    public int Quantity { get; set; }
    // Constructor
    public AddItemCommand(string name, int quantity)
    {
        Name = name;
        Quantity = quantity;
    }
}
Public Class AddItemCommand
	Public Property Name() As String
	Public Property Quantity() As Integer
	' Constructor
	Public Sub New(ByVal name As String, ByVal quantity As Integer)
		Me.Name = name
		Me.Quantity = quantity
	End Sub
End Class
VB   C#

And the AddItemCommandHandler:

public class AddItemCommandHandler : ICommandHandler<AddItemCommand>
{
    public void Handle(AddItemCommand command)
    {
        // Here, you'd add the item to your database, for example, to have employee data stored
        Console.WriteLine($"Adding item: {command.Name} with quantity {command.Quantity}");
        // Add database logic here
    }
}
public class AddItemCommandHandler : ICommandHandler<AddItemCommand>
{
    public void Handle(AddItemCommand command)
    {
        // Here, you'd add the item to your database, for example, to have employee data stored
        Console.WriteLine($"Adding item: {command.Name} with quantity {command.Quantity}");
        // Add database logic here
    }
}
Public Class AddItemCommandHandler
	Implements ICommandHandler(Of AddItemCommand)

	Public Sub Handle(ByVal command As AddItemCommand)
		' Here, you'd add the item to your database, for example, to have employee data stored
		Console.WriteLine($"Adding item: {command.Name} with quantity {command.Quantity}")
		' Add database logic here
	End Sub
End Class
VB   C#

Your GetItemsQuery could be empty if it doesn't need any parameters to fetch tasks, and GetItemsQueryHandler might look like:

public class GetItemsQuery
{
    // This class might not need any properties, depending on your query
}
using CQRS_testing.Interfaces;
namespace CQRS_testing.Queries
{
    public class GetItemsQueryHandler : IQueryHandler<GetItemsQuery, IEnumerable<string>>
    {
        public IEnumerable<string> Handle(GetItemsQuery query)
        {
            // Here, you'd fetch items from your database
            return new List<string> { "Item1", "Item2" };
        }
    }
}
public class GetItemsQuery
{
    // This class might not need any properties, depending on your query
}
using CQRS_testing.Interfaces;
namespace CQRS_testing.Queries
{
    public class GetItemsQueryHandler : IQueryHandler<GetItemsQuery, IEnumerable<string>>
    {
        public IEnumerable<string> Handle(GetItemsQuery query)
        {
            // Here, you'd fetch items from your database
            return new List<string> { "Item1", "Item2" };
        }
    }
}
Imports CQRS_testing.Interfaces

Public Class GetItemsQuery
	' This class might not need any properties, depending on your query
End Class
Namespace CQRS_testing.Queries
	Public Class GetItemsQueryHandler
		Implements IQueryHandler(Of GetItemsQuery, IEnumerable(Of String))

		Public Function Handle(ByVal query As GetItemsQuery) As IEnumerable(Of String)
			' Here, you'd fetch items from your database
			Return New List(Of String) From {"Item1", "Item2"}
		End Function
	End Class
End Namespace
VB   C#

In your ASP.NET controllers, you'll use these handlers to process commands and queries. For adding a task, the controller action would create an AddTaskCommand, set its properties from the form data, and then pass it to an AddTaskCommandHandler instance to handle. For retrieving tasks, it would call a GetTasksQueryHandler to get the data and pass it to the view.

Wiring it Up in a Controller

With your commands and queries set up, you can now use them in your controllers. Here's how you might do that in an ItemsController class:

public class ItemsController : Controller
{
    private readonly ICommandHandler<AddItemCommand> _addItemHandler;
    private readonly IQueryHandler<GetItemsQuery, IEnumerable<string>> _getItemsHandler;
    // Constructor injection is correctly utilized here
    public ItemsController(ICommandHandler<AddItemCommand> addItemHandler, IQueryHandler<GetItemsQuery, IEnumerable<string>> getItemsHandler)
    {
        _addItemHandler = addItemHandler;
        _getItemsHandler = getItemsHandler;
    }
    public IActionResult Index()
    {
        // Use the injected _getItemsHandler instead of creating a new instance
        var query = new GetItemsQuery();
        var items = _getItemsHandler.Handle(query);
        return View(items);
    }
    [HttpPost]
    public IActionResult Add(string name, int quantity)
    {
        // Use the injected _addItemHandler instead of creating a new instance
        var command = new AddItemCommand(name, quantity);
        _addItemHandler.Handle(command);
        return RedirectToAction("Index");
    }
}
public class ItemsController : Controller
{
    private readonly ICommandHandler<AddItemCommand> _addItemHandler;
    private readonly IQueryHandler<GetItemsQuery, IEnumerable<string>> _getItemsHandler;
    // Constructor injection is correctly utilized here
    public ItemsController(ICommandHandler<AddItemCommand> addItemHandler, IQueryHandler<GetItemsQuery, IEnumerable<string>> getItemsHandler)
    {
        _addItemHandler = addItemHandler;
        _getItemsHandler = getItemsHandler;
    }
    public IActionResult Index()
    {
        // Use the injected _getItemsHandler instead of creating a new instance
        var query = new GetItemsQuery();
        var items = _getItemsHandler.Handle(query);
        return View(items);
    }
    [HttpPost]
    public IActionResult Add(string name, int quantity)
    {
        // Use the injected _addItemHandler instead of creating a new instance
        var command = new AddItemCommand(name, quantity);
        _addItemHandler.Handle(command);
        return RedirectToAction("Index");
    }
}
Public Class ItemsController
	Inherits Controller

	Private ReadOnly _addItemHandler As ICommandHandler(Of AddItemCommand)
	Private ReadOnly _getItemsHandler As IQueryHandler(Of GetItemsQuery, IEnumerable(Of String))
	' Constructor injection is correctly utilized here
	Public Sub New(ByVal addItemHandler As ICommandHandler(Of AddItemCommand), ByVal getItemsHandler As IQueryHandler(Of GetItemsQuery, IEnumerable(Of String)))
		_addItemHandler = addItemHandler
		_getItemsHandler = getItemsHandler
	End Sub
	Public Function Index() As IActionResult
		' Use the injected _getItemsHandler instead of creating a new instance
		Dim query = New GetItemsQuery()
		Dim items = _getItemsHandler.Handle(query)
		Return View(items)
	End Function
	<HttpPost>
	Public Function Add(ByVal name As String, ByVal quantity As Integer) As IActionResult
		' Use the injected _addItemHandler instead of creating a new instance
		Dim command = New AddItemCommand(name, quantity)
		_addItemHandler.Handle(command)
		Return RedirectToAction("Index")
	End Function
End Class
VB   C#

To wire everything up, especially if you're using Dependency Injection (DI) in ASP.NET Core, you'll need to register your command and query handlers with the DI container in the Startup.cs file. This way, ASP.NET can provide instances of your handlers when they're needed.

Here's a very basic example of registering a handler:

builder.Services.AddTransient<ICommandHandler<AddItemCommand>, AddItemCommandHandler>();
builder.Services.AddTransient<IQueryHandler<GetItemsQuery, IEnumerable<string>>, GetItemsQueryHandler>();
builder.Services.AddTransient<ICommandHandler<AddItemCommand>, AddItemCommandHandler>();
builder.Services.AddTransient<IQueryHandler<GetItemsQuery, IEnumerable<string>>, GetItemsQueryHandler>();
builder.Services.AddTransient(Of ICommandHandler(Of AddItemCommand), AddItemCommandHandler)()
builder.Services.AddTransient(Of IQueryHandler(Of GetItemsQuery, IEnumerable(Of String)), GetItemsQueryHandler)()
VB   C#

In the practical application of CQRS, the distinction between the data model for write operations and the one for read operations is foundational, ensuring that the architecture supports varied and optimized approaches to handling data.

IronPDF: C# PDF Library

CQRS Pattern C# (How It Works For Developers): Figure 3 - IronPDF webpage

IronPDF is a tool for developers working with the C# programming language, allowing them to create, read, and edit PDF documents directly within their applications. This library is user-friendly, making it simpler to integrate PDF functionalities such as generating PDF reports, invoices, or forms from HTML code. IronPDF supports various features, including editing text and images in PDFs, setting up document security, and converting webpages to PDF format. Its versatility and ease of use make it a valuable resource for developers looking to implement PDF operations in their projects.

Code Example

Now, let's explore how IronPDF can be utilized within a C# application following the Command Query Responsibility Segregation (CQRS) pattern. Below is a simplified example demonstrating how you might use IronPDF within a CQRS setup to generate a PDF report. This example is conceptual and focuses on the generation of a PDF document as a command.

using IronPdf;
using System.Threading.Tasks;
namespace PdfGenerationApp.Commands
{
    public class GeneratePdfReportCommand
    {
        // Command handler that generates a PDF report
        public async Task GenerateReportAsync(string reportContent, string outputPath)
        {
            // Initialize the IronPDF HTML to PDF renderer
            var renderer = new ChromePdfRenderer();
            // Use IronPDF to generate a PDF from HTML content
            var pdf = await Task.Run(() => renderer.RenderHtmlAsPdf(reportContent));
            // Save the generated PDF to a specified path
            pdf.SaveAs(outputPath);
        }
    }
}
using IronPdf;
using System.Threading.Tasks;
namespace PdfGenerationApp.Commands
{
    public class GeneratePdfReportCommand
    {
        // Command handler that generates a PDF report
        public async Task GenerateReportAsync(string reportContent, string outputPath)
        {
            // Initialize the IronPDF HTML to PDF renderer
            var renderer = new ChromePdfRenderer();
            // Use IronPDF to generate a PDF from HTML content
            var pdf = await Task.Run(() => renderer.RenderHtmlAsPdf(reportContent));
            // Save the generated PDF to a specified path
            pdf.SaveAs(outputPath);
        }
    }
}
Imports IronPdf
Imports System.Threading.Tasks
Namespace PdfGenerationApp.Commands
	Public Class GeneratePdfReportCommand
		' Command handler that generates a PDF report
		Public Async Function GenerateReportAsync(ByVal reportContent As String, ByVal outputPath As String) As Task
			' Initialize the IronPDF HTML to PDF renderer
			Dim renderer = New ChromePdfRenderer()
			' Use IronPDF to generate a PDF from HTML content
			Dim pdf = Await Task.Run(Function() renderer.RenderHtmlAsPdf(reportContent))
			' Save the generated PDF to a specified path
			pdf.SaveAs(outputPath)
		End Function
	End Class
End Namespace
VB   C#

In this example, GeneratePdfReportCommand represents a command in the CQRS pattern. It includes a method GenerateReportAsync that takes in reportContent as an HTML string and an outputPath where the PDF report will be saved. IronPDF's HtmlToPdf class is used to convert the HTML content to PDF format, which is then saved to the specified path. This setup illustrates how you can integrate PDF generation functionality into your application's architecture, especially in scenarios requiring clear separation of concerns as promoted by CQRS.

CQRS Pattern C# (How It Works For Developers): Figure 4 - Outputted PDF

Conclusion

CQRS Pattern C# (How It Works For Developers): Figure 5 - IronPDF license page

To wrap up, the Command Query Responsibility Segregation (CQRS) pattern offers a structured approach to separating the responsibilities of reading and writing data in your applications. This separation not only clarifies the architecture but also enhances the flexibility, scalability, and performance of your systems. By following the steps outlined above, you can implement CQRS in your ASP.NET Core applications, using tools like MediatR to streamline communication between commands, queries, and their handlers.

Integrating IronPDF into your CQRS-based application further expands its capabilities, enabling you to effortlessly create, manipulate, and store PDF documents. Whether you're generating reports, invoices, or any form of document, IronPDF's comprehensive features and straightforward syntax make it a powerful tool in your development toolkit. IronPDF offers a free trial, giving you the chance to explore its capabilities before committing. For continued use, licenses start from $749, providing various options to match your project's needs.