Using the CQRS Pattern in C#

26 mei 2021 om 10:00 by ParTech Media - Post a comment

Most developers are accustomed to working with CRUD operations(Create, Read, Update and Delete) that help to divide tasks in a system. But the complications that this practice brings in due to different frameworks, solutions, and sophisticated infrastructure, makes the work of developers tedious.

One of the main concerns regarding this is the increasing number of representations your model can have when the application must serve more and more clients. Most of the time, the architectural designs of the application are made in a CRUD-like way which means a single database(irrespective of being relational or not) is used both for storing and serving as the querying center for all requirements.

The concern about putting these sides together especially when a microservice is born, mandates the developers to create each entity in a specialized manner when it comes to performance issues, scalability independence, and resilience issues.

The solution to this is a CQRS pattern. In this post, we will look into what is CQRS pattern and how it is useful.

Table of Contents

  1. What is CRUD?
  2. What is CQRS?
  3. Features of CQRS
  4. Why CQRS over CRUD?
  5. Working of CQRS
  6. Practical implementations of CQRS
  7. Conclusion

What is CRUD?

When building APIs, we want our application to perform the following four functionalities: Create, Read, Update and Delete. This acronym is called CRUD. However, CRUD operations are performed over a single database and data contentions are common when operated for enterprise-scale applications.

In traditional architectures, a single data model is used both for reading and updating a database. That sounds simple and works for basic CRUD-like operations. However, this approach can become unwieldy in complex enterprise applications. For instance, the read operation may return different data transfer objects with different shapes. On the right side, complex validation and business logic are involved. Hence, object mapping becomes complicated. You end up with a model that is overly complicated and that does too much.

To resolve this, CQRS is deployed. Let us look at what CQRS is and why it is being used.

What is CQRS?

CQRS stands for Command and Query Responsibility Segregation. It is a command query pattern and can separate the read and write operations for a data store. CQRS uses a command and query pattern in which the command functionality only updates or writes data and the query functionality returns the data. Hence two different entities are used to read and write data. It becomes easy to distinguish the transfer and retrieval of data. Using CQRS in your application can maximize the performance, security, and scalability of your application.

Features of CQRS

  1. Independent scaling: CQRS enables separate read and write workloads. This results in fewer collision contentions.
  2. Optimized schemas(abstract database designs): The read operation uses a schema that is optimized for queries, while the write operation uses a schema that is optimized for updates.
  3. Enhanced security: It is easier to ensure that the right domain entity(might be commands or queries) performs the respective read and write operations.
  4. Ruling out complexity: The segregation of read and write entities rules out complexity and hence the application becomes flexible and maintainable. Most of the complex business logic makes its way into the write operation. The read operation is relatively simple.
  5. Simpler queries: By storing a materialized view of data in read database, the application can avoid joins that require complex querying.

Why CQRS over CRUD?

Read and write workloads are often asymmetrical in CRUD operations, differing in performance and scalability. Data contention occurs when both operations are performed simultaneously on the same data set in CRUD. The traditional approach can have negative ripples on the performance of your application due to the load on the data store and data access layer as read and write operations are performed using a single data model. Managing security and permissions for the applications is cumbersome, as each entity in the application is subjected to both read and write data and can expose information in the wrong context.

In the CQRS pattern, commands are task-centric, rather than data-centric. Command patterns only alter the state of the data (that is write) but do not return data. Also, asynchronous processing is followed rather than synchronous processes. The best part is ‘queries’ in CQRS never modify the data. The query returns a DTO that does not overlap with the commands.

Working of CQRS

Here, the command and query operations are occurring simultaneously but do not cause conflicts and are used for separate purposes. Having separate interfaces for read and write operations greatly simplifies the design of your application.

In the above diagram, commands are used to perform the write operation and the queries are used to perform the read operation ultimately projecting it onto the user interface model.

Practical implementation of CQRS

Step 1

Open visual studio and create a new project. Select Console APP(.NET Core) by filtering as in the below image.

Step 2

Configure your project by giving the title of the project and other specifications. Create folders and repositories as given below to implement the CQRS pattern in C#.

Let us now look into the working and implementation of CQRS. In the below program, only write operations are included which comprises an interface and write repository.

The commands(write) module

using ConsoleAppCQRSPattern.Models;  

namespace ConsoleAppCQRSPattern.Repositories {  

  public interface IEmployeeCommandsRepository {  

​    void SaveEmployee(Employee employee);  

 }  

}  

using ConsoleApp CQRSPattern.Models;  

namespace ConsoleApp CQRSPattern.Repositories {  

 public class EmployeeCommandsRepository: IEmployeeCommandsRepository {  

​    public void SaveEmployee(Employee employee) {  

   }  

  }  

}  



The below program comprises only the read operations that constitute an interface and repository for reading and returning data.

Queries(read) module

using PartechCQRS.DTOs;

namespace PartechCQRS.Queries

{

  public interface IEmployeeQueries

  {

​    EmployeeDTO FindByID(int employeeID);

  }

}

using System;  

using ConsoleAppCQRSPattern.Models;  

namespace ConsoleAppCQRSPattern.Repositories {  

public class EmployeeQueriesRepository: IEmployeeQueriesRepository {  

public Employee GetByID(int employeeID) {  



// Below is for demo purposes only  

 return new Employee()

 {  

 Id = 100,  

 FirstName = "John",  

 LastName = "Smith",  

 DateOfBirth = new DateTime(2000, 1, 1),  

 Street = "100 Toronto Street",  

 City = "Toronto",  

ZipCode = "k1k 1k1"  

 };  

 }  

 }

 } 


Next, we will look at the two middle-tier components of read and write respectively.

Queries(read) repository

using PartechCQRS.Models;

namespace PartechCQRS.Repositories

{

  public interface IEmployeeQueriesRepository

  {

​    Employee GetByID(int employeeID);

  }

}


using System;

using PartechCQRS.Repositories;

using PartechCQRS.DTOs;

namespace PartechCQRS.Queries

{

  public class EmployeeQueries

  {

​    private readonly IEmployeeQueriesRepository _repository;

​    public EmployeeQueries(IEmployeeQueriesRepository repository)

​    {

​      _repository = repository;

​    }

​    public EmployeeDTO FindByID(int employeeID)

​    {

​      var emp = _repository.GetByID(employeeID);

​      return new EmployeeDTO

​      {

​        Id = emp.Id,

​        FullName = emp.FirstName + " " + emp.LastName,

​        Address = emp.Street + " " + emp.City + " " + emp.ZipCode,

​        Age = Convert.ToInt32(Math.Abs(((DateTime.Now - emp.DateOfBirth).TotalDays) / 365)) - 1

​      };

​    }

  }

}


Commands(write) repository

Using PartechCQRS.Models;

namespace PartechCQRS.Commands

{

  public interface IEmployeeCommands

  {

​    void SaveEmployeeData(Employee employee);

  }

}  


using PartechCQRS.Models;  

using PartechCQRS.Repositories;  

namespace PartechCQRS.Commands

{

  public class EmployeeCommands : IEmployeeCommands

  {

​    private readonly IEmployeeCommandsRepository _repository;

​    public EmployeeCommands(IEmployeeCommandsRepository repository)

​    {

​      _repository = repository;

​    }

​    public void SaveEmployeeData(Employee employee)

​    {

​      _repository.SaveEmployee(employee);

​    }

  }

}

Here it is clear that we have different operation handling components. The two other classes are the main employee class which mirrors our storage item and the DTO class. This is needed by the Queries(read) operation to return data in the prescribed format.

namespace PartechCQRS.DTOs

{

  public class EmployeeDTO

  {

​    public int Id

​    {

​      get;

​      set;

​    }

​    public string FullName

​    {

​      get;

​      set;

​    }

​    public int Age

​    {

​      get;

​      set;

​    }

​    public string Address

​    {

​      get;

​      set;

​    }

  }

}





​      using System;

namespace PartechCQRS.Models

{

  public class Employee

  {

​    public int Id

​    {

​      get;

​      set;

​    }

​    public string FirstName

​    {

​      get;

​      set;

​    }

​    public string LastName

​    {

​      get;

​      set;

​    }

​    public DateTime DateOfBirth

​    {

​      get;

​      set;

​    }

​    public string Street

​    {

​      get;

​      set;

​    }

​    public string City

​    {

​      get;

​      set;

​    }

​    public string ZipCode

​    {

​      get;

​      set;

​    }

  }

}

Finally, we call these above operations from the main program class. It creates objects to access and save data returned using queries(read) operation and display the data using commands(write) operation.

using PartechCQRS.Queries;

using PartechCQRS.Repositories;

using System;


namespace PartechCQRS

{  

  class Program {  

​    static void Main(string[] args)

  {

​    // Command the Employee Domain to save data  

​    var employeeCommand = new EmployeeCommandsRepository();

   

​    Console.WriteLine( "Employee data stored");

​    // Query the Employee Domain to get data  

​    var employeeQuery = new EmployeeQueries(new EmployeeQueriesRepository());

​    var emp = employeeQuery.FindByID(100);

​      Console.WriteLine("Employee ID:{employee.Id}, Name:{employee.FullName}, Address:{employee.Address}, Age:{employee.Age}"); Console.ReadKey();

  }

}  

}  

Hence the queries and commands work independently by reading and writing the data by creating objects for the saveEmployeeData class and retrieving it.

Conclusion

CQRS design pattern is a holistic approach that greatly simplifies the design and eliminates redundancies and complexities compared to traditional CRUD operations. While building complex enterprise applications make sure that you implement the CQRS pattern to avoid complexities.

Nieuwste