Understanding CQRS architecture - Part 3

In this post, we implement the CRUD architectural style, scrutinize its strengths and weaknesses, and explore situations where it becomes imperative to replace it with a more sophisticated pattern.

The CRUD architectural style is extensively utilized and predominantly emphasizes patterns where there is a single model for both queries and commands.

We will apply this pattern in a fictional company tasked with developing two straightforward services: one responsible for storing all reference data (countries, currencies, etc.) and another managing identity and the authentication process.

CRUD for the ReferenceData service

The ReferenceData service is responsible for managing information used throughout the entire organization, including countries, currencies, departments, etc. We will implement it using an Azure Function and code a few CRUD methods.

  • Define a Country class aimed at modeling a country for example
1public class Country
2{
3    public string Id { get; set; }
4    public string Name { get; set; }
5}
  • Define some useful methods in a repository to find or save a country
 1public class CountryRepository : ICountryRepository
 2{
 3    public async Task<Country> FindById(string id)
 4    {
 5        // ...
 6    }
 7	
 8    public Task SaveCountry(Country country)
 9    {
10        // ...
11    }
12}
  • Below is an example of an Azure Function utilizing this repository.
 1[FunctionName(nameof(FindCountryById))]
 2public async Task<IActionResult> GetAccountById([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log)
 3{
 4    var id = req.Query["countryId"]; 
 5
 6    var repository = new CountryRepository(); // In a real-world application, use DI.
 7    return new OkObjectresult(await repository.FindById(id).ConfigureAwait(false));
 8}
 9
10[FunctionName(nameof(SaveCountry))]
11public async Task<IActionResult> SaveCountry([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, ILogger log)
12{
13    var body = await new StreamReader(req.Body).ReadToEndAsync();
14    var data = JsonConvert.DeserializeObject<List<SaveCountryRequest>>(body);
15
16    var country = new Country()
17    {
18        Id = data.Id,
19        Name = data.Name
20    };
21
22    var repository = new CountryRepository(); // In a real-world application, use DI.
23    await repository.SaveCountry(country).ConfigureAwait(false);
24
25    return new OkResult();
26}
Information

Certain classes and interfaces (ICountryRepository, etc...) have not been developed here for the sake of conciseness.

This approach is perfectly suitable in this case because the requirements for both read and write operations are the same. When a country is requested, the client application only requires an id and a name. Similarly, for creating or editing a country, an id and a name are the only needed attributes. Separating the two would be unnecessary complexity in this context.

CRUD for the Membership service

Now, let's contemplate a more intricate scenario wherein an administrator registers a new user (with an email and a password) and subsequently can view all these registered users in their dashboard. Once again, we initiate the process just mentioned for the ReferenceData service.

  • Define a User class aimed at modeling a user
1public class User
2{
3    public string Email { get; set; }
4    public string Password { get; set; }
5}
  • Define a method in a repository to create a user
1public class UserRepository : IUserRepository
2{	
3    public Task SaveUser(User user)
4    {
5        // ...
6    }
7}
  • Define a method in the same repository to find all users
1public class UserRepository : IUserRepository
2{		
3    public Task<List<User>> FindUsers()
4    {
5        // ...
6    }
7}
  • Below is an example of an Azure Function utilizing this repository.
 1[FunctionName(nameof(FindAllUsers))]
 2public async Task<IActionResult> FindAllUsers([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log)
 3{
 4    var repository = new UserRepository(); // In a real-world application, use DI.
 5    return new OkObjectresult(await repository.FindUsers().ConfigureAwait(false));
 6	
 7    //
 8    // Passwords are returned to the client !!!
 9    //
10}
11
12[FunctionName(nameof(SaveUser))]
13public async Task<IActionResult> SaveUser([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, ILogger log)
14{
15    var body = await new StreamReader(req.Body).ReadToEndAsync();
16    var data = JsonConvert.DeserializeObject<List<SaveUserRequest>>(body);	
17
18    var repository = new UserRepository(); // In a real-world application, use DI.
19    await repository.SaveUser(data.ConvertToUser()).ConfigureAwait(false);
20
21    return new OkResult();
22}

However, a problem arises in the FindAllUsers function: the users' passwords are returned to the client, making them susceptible to interception by attackers. More critically, even an administrator should not have access to the credentials of their clients. This constitutes a significant security flaw.

What is the issue exactly ?

The fundamental issue arises because we need to carry out two distinct operations with different requirements using the same model:

  • The write model must store emails and passwords to enable users to authenticate in subsequent login attempts.
  • The read model involves more than just displaying all the attributes of the user; it encompasses adhering to security requirements and enforcing confidentiality rules.

Why not simply limit the attributes to retrieve in the FindUsers method ?

If the data is stored in a relational database, we can, for example, envision coding the retrieval of the user's attributes as follows.

1SELECT t.Email --Password is not returned.
2FROM Users 

This way, by not retrieving the password from the database, it is indeed not returned to the client. While this is a solution, in a hurry, an absent-minded developer might one day modify this code as follows.

1SELECT *
2FROM Users 

Hence, this demands a stringent discipline that is not always sustainable in the long term.

We could also handle this logic directly within the Azure Function.

 1[FunctionName(nameof(FindAllUsers))]
 2public async Task<IActionResult> FindAllUsers([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log)
 3{
 4    var repository = new UserRepository(); // In a real-world application, use DI.
 5    var users = await repository.FindUsers().ConfigureAwait(false);
 6	
 7    users.ToList().ForEach(x => 
 8    {
 9        x.Password = "";
10    });	
11	
12    return new OkObjectresult(users);
13}

Once again, this is a solution, but the business logic is entangled with the technical code. For non-critical applications, it would be perfectly acceptable, but for typical line-of-business applications, we need to find a better alternative. CQRS to the rescue !

Implementing a CQRS architecture in C# - Part 4