Create you’re first CQRS webapi

CQRS is short for Command Query Responsibility Segregation. A couple of years ago, when I just started web programming, I came in contact with this design pattern. This pattern separates read and update operations for a data store. Read here for the basic description.

1. How to start?

First step it to create a new webapi project (Preferably .NET5). I named it GJ.CQRSCore.Example.

1.1 Setup basic structure

The next step is to install my NuGet Package GJ.CQRSCore. This is my personal framework to use for CQRSCore. In a next blog I will discuss the parts that are in this core library. Also add the following empty .NET Standard projects to the solution with corresponding folders:

  • GJ.CQRSCore.Example.BusinessLogic
    • CommandHandlers
    • QueryHandlers
    • Validators
  • GJ.CQRSCore.Example.Data
    • Interfaces
    • Models
  • GJ.CQRSCore.Example.Models
    • Commands
    • Queries

1.2 Setup Data layer

We create a basic data layer that stores it’s information in statics. Ofcourse that is not a real life situation. You can replace this yourself with EntityFramework, MongoDb or another database. Copy the GJ.CQRSCore.Example.Data project to you’re own project.

No we are going to configure the data layer in the startup.cs. Add in the method configure services the following lines of code:

services.AddScoped<ICompanyRepository, CompanyRepository>();
services.AddScoped<IOfficeRepository, OfficeRepository>();

And add the two repositories in the Configure method and add GenerateStartUpInfo to the method.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, 
            ICompanyRepository companyRepo, 
            IOfficeRepository officeRepo)
{
    TestData.GenerateStartupInfo(companyRepo, officeRepo);
    ###
}

Now whe have some testdata, that is loaded on start-up.

2. Create you’re first query

In this chapter we are creating our first query. Queries are used for getting information. We want a list with all the companies with their CEO’s.

2.1 Creating the response model

First we create the model we want to receive as a response. We should put this in “GJ.CQRSCore.Example.Models”:

using System;

namespace GJ.CQRSCore.Example.Models
{
    public class CompanyCeoModel
    {
        public Guid CompanyId { get; set; }
        public string CompanyName { get; set; }
        public string CEO { get; set; }
    }
}

2.2 Creating the QueryModel

We are now creating the model, that we are going to use to query the backend. We create a class that inherits from the IQuery interface. IQuery is part of GJ.CQRSCore. With this interface it can be used in the coming steps.

Add the following class to “GJ.CQRSCore.Example.Models.Queries”:

using GJ.CQRSCore.Interfaces;

namespace GJ.CQRSCore.Example.Models.Queries
{
    public class GetCompanyWithCeoListQuery : IQuery
    {
    }
}

Alert

Be as descriptive as possible for naming commands and queries. This makes CQRS most readable.

2.3 Adding the QueryHandler

Now we are adding the businesslogic in the queryhandler. This queryhandler inherits from QueryHandlerBase. The first parameter of QueryHandlerBase is the inputquery defined in step 2.2. The second parameter is the expected result of the method. Now add the following file in “GJ.CQRSCore.Example.BusinessLogic.QueryHandlers”:

using GJ.CQRSCore.Example.Models;
using GJ.CQRSCore.Example.Models.Queries;
using System.Collections.Generic;

namespace GJ.CQRSCore.Example.BusinessLogic.QueryHandlers
{
    public class GetCompanyWithCeoListQueryHandler : QueryHandlerBase<GetCompanyWithCeoListQuery, IList<CompanyCeoModel>>
    {
        public override IList<CompanyCeoModel> Handle(GetCompanyWithCeoListQuery query)
        {
            return new List<CompanyCeoModel>();
        }
    }
}

The next part is adding the business logic in the GetCompanyWithCeoListQueryHandler. First we need to introduce the constructor that injects ICompanyRepository.

private readonly ICompanyRepository _companyRepository;

public GetCompanyWithCeoListQueryHandler(ICompanyRepository companyRepository)
{
    _companyRepository = companyRepository;
}

Now we can add the logic to the handle function. We need to use companyRepository to get the info from dat datalayer:

public override IList<CompanyCeoModel> Handle(GetCompanyWithCeoListQuery query)
{
    return _companyRepository.GetAll().Select(x=> new CompanyCeoModel()
    {
        CompanyId = x.Id,
        CompanyName = x.Name,
        CEO = x.CEO
    }).ToList();
}

2.4 Registering the QueryHandler in the dependency injection

We are going back to the startup.cs to register the queryhandler in dependency injection. Add the following line of code in the method that is called ConfigureServices:

services.AddScoped<IQueryHandler<GetCompanyWithCeoListQuery, IList<CompanyCeoModel>>, GetCompanyWithCeoListQueryHandler>();

2.5 Adding the controller

We first start with adding a new controller named “CompanyController”.

using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;

namespace GJ.CQRSCore.Example.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class CompanyController : ControllerBase
    {

    }
}

Then we are going add a constructor that injects IQueryHandler<GetCompanyWithCeoListQuery, IList<CompanyCeoModel>>:

private readonly IQueryHandler<GetCompanyWithCeoListQuery, IList<CompanyCeoModel>> _getCompanyWithCeoListQueryHandler;
public CompanyController(IQueryHandler<GetCompanyWithCeoListQuery, IList<CompanyCeoModel>> getCompanyWithCeoListQueryHandler)
{
    _getCompanyWithCeoListQueryHandler = getCompanyWithCeoListQueryHandler;
}

The next part is adding a method that calls this queryhandler:

[HttpGet("GetCompanyWithCeoListQuery")]
public IEnumerable<CompanyCeoModel> Get()
{
    return _getCompanyWithCeoListQueryHandler.Execute(new GetCompanyWithCeoListQuery());
}

Now we can run the solution. When you browse to /company/GetCompanyWithCeoListQuery or use postman, you get response in json format with two companies and their according CEO’s.

3. Create you’re first command

In this chapter we are creating our first command. Commands are used We want to add a company with an adress and the command should automatically add a company and a office.

3.1 Creating the CommandModel

We are now creating the model, that we are going to use to send the command to the backend. We create a class that inherits from the ICommand interface. ICommand is part of GJ.CQRSCore. With this interface it can be used in the coming steps.

Add the following class to “GJ.CQRSCore.Example.Models.Commands”:

using GJ.CQRSCore.Interfaces;
using System;

namespace GJ.CQRSCore.Example.Models.Commands
{
    public class AddCompanyWithOfficeCommand : ICommand
    {
        public string CompanyName { get; set; }
        public string CEO { get; set; }
        public string BuildingName { get; set; }
        public string Street { get; set; }
        public int HouseNumber { get; set; }
        public string City { get; set; }
    }
}

3.2 Adding the CommandHandler

Now we are adding the businesslogic in the commandhandler. This commandhandler inherits from CommandHandlerBase. The first parameter of CommandHandlerBase is the inputcommand defined in step 3.1. Now add the following file in “GJ.CQRSCore.Example.BusinessLogic.CommandHandlers”:

using GJ.CQRSCore.Example.Data.Interfaces;
using GJ.CQRSCore.Example.Data.Models;
using GJ.CQRSCore.Example.Models.Commands;
using System;

namespace GJ.CQRSCore.Example.BusinessLogic.CommandHandlers
{
    public class AddCompanyWithOfficeCommandHandler : CommandHandlerBase<AddCompanyWithOfficeCommand>
    {
        private readonly ICompanyRepository _companyRepository;
        private readonly IOfficeRepository _officeRepository;

        public AddCompanyWithOfficeCommandHandler(ICompanyRepository companyRepository, IOfficeRepository officeRepository)
        {
            _companyRepository = companyRepository;
            _officeRepository = officeRepository;
        }

        public override void Handle(AddCompanyWithOfficeCommand command)
        {
            var office = new Office() {
                Id = Guid.NewGuid(),
                BuildingName = command.BuildingName,
                Street = command.Street,
                HouseNumber = command.HouseNumber,
                City = command.City
            };
            var company = new Company() {
                Id = Guid.NewGuid(),
                Name = command.CompanyName,
                CEO = command.CEO,
            };
            company.OfficeIds.Add(office.Id);

            _companyRepository.Add(company);
            _officeRepository.Add(office);
        }
    }
}

3.3 Registering the CommandHandler in the dependency injection

We are going back to the startup.cs to register the queryhandler in dependency injection. Add the following line of code in the method that is called ConfigureServices:

services.AddScoped<ICommandHandler<AddCompanyWithOfficeCommand>, AddCompanyWithOfficeCommandHandler>();

3.4 Expand the controller

Now we are going to expand the controller with the new command. We start with expanding the constructor:

private readonly IQueryHandler<GetCompanyWithCeoListQuery, IList<CompanyCeoModel>> _getCompanyWithCeoListQueryHandler;
private readonly ICommandHandler<AddCompanyWithOfficeCommand> _addCompanyWithOfficeCommandHandler;

public CompanyController(
     IQueryHandler<GetCompanyWithCeoListQuery, IList<CompanyCeoModel>> getCompanyWithCeoListQueryHandler,
     ICommandHandler<AddCompanyWithOfficeCommand> addCompanyWithOfficeCommandHandler)
{
     _getCompanyWithCeoListQueryHandler = getCompanyWithCeoListQueryHandler;
     _addCompanyWithOfficeCommandHandler = addCompanyWithOfficeCommandHandler;
}

The next step is adding the new operator:

[HttpPost("AddCompanyWithOfficeCommand")]
public IActionResult AddCompanyWithOfficeCommand([FromBody] AddCompanyWithOfficeCommand command)
{
     if (command == null) throw new ArgumentNullException();

     _addCompanyWithOfficeCommandHandler.Execute(command);

    return Ok();
}

3.5 Testing the controller

Last step is testing the controller. We do this by starting the application and run postman. We are going to create a request with the following URL: “https://localhost:44356/company/AddCompanyWithOfficeCommand”. In the body we will the following json:

{
    "CompanyName": "TestCompany",
    "CEO": "Tester Test",
    "BuildingName": "Test Location",
    "Street": "Test Street",
    "Housenumber": 1,
    "City": "TestCity"
}

Now we can check if the command has succesfully run, by opening the browser on “https://localhost:44356/company/GetCompanyWithCeoListQuery”.

URL

I used the url https:///localhost:44356. This depends on the config of your solution.

4. Create you’re first validator

We don’t want any companies that have no name or a double name. Also we don’t want any offices with no fields filled or multiple entries on the same adress. That’s why we need a validator with validation logic.

4.1 Create the validator

We start with creating a empty validator class in “GJ.CQRSCore.Example.BusinessLogic.Validators”:

using GJ.CQRSCore.Example.Models.Commands;
using GJ.CQRSCore.Interfaces;
using GJ.CQRSCore.Validation;

namespace GJ.CQRSCore.Example.BusinessLogic.Validator
{
    public class AddCompanyWithOfficeCommandValidator : IValidator<AddCompanyWithOfficeCommand>
    {
        public ValidationResults Validate(ValidationResults results, AddCompanyWithOfficeCommand validatableObject)
        {

            return results;
        }
    }
}

We will add the logic for checking the values on not null or empty. Add the following code to the validate method and:

results = ValidateNoNullOrEmptyValues(results, validatableObject);

And add the following method to the AddCompanyWithOfficeCommandValidator.cs:

private static ValidationResults ValidateNoNullOrEmptyValues(ValidationResults results, AddCompanyWithOfficeCommand validatableObject)
{
    results.ValidateNotNullOrEmpty(validatableObject.CompanyName, nameof(validatableObject.CompanyName));
    results.ValidateNotNullOrEmpty(validatableObject.CEO, nameof(validatableObject.CEO));
    results.ValidateNotNullOrEmpty(validatableObject.BuildingName, nameof(validatableObject.BuildingName));
    results.ValidateNotNullOrEmpty(validatableObject.Street, nameof(validatableObject.Street));
    results.ValidateNotNull(validatableObject.HouseNumber, nameof(validatableObject.HouseNumber));
    results.ValidateNotNullOrEmpty(validatableObject.City, nameof(validatableObject.City));
    return results;
}

The next validation is to add the other validations. Now add the following constructor to the AddCompanyWithOfficeCommandValidator.cs so we can call the ICompanyRepository and the IOfficeRepository:

private readonly ICompanyRepository _companyRepository;
private readonly IOfficeRepository _officeRepository;
public AddCompanyWithOfficeCommandValidator(ICompanyRepository companyRepository, IOfficeRepository officeRepository)
{
    _companyRepository = companyRepository;
    _officeRepository = officeRepository;
}

Add the following code to the validate method:

results = ValidateCompanyNameDoesntExist(results, validatableObject);
results = ValidateAddressAlreadyExists(results, validatableObject);

And add the following methods to the AddCompanyWithOfficeCommandValidator.cs:

private ValidationResults ValidateAddressAlreadyExists(ValidationResults results, AddCompanyWithOfficeCommand validatableObject)
{
   if (_officeRepository.GetAll().Any(x => x.Street == validatableObject.Street && x.HouseNumber == validatableObject.HouseNumber && x.City == validatableObject.City))
   {
       results.AddValidationResult(nameof(validatableObject.CompanyName), "The adress already exists in the database.");
   }
   return results;
}

private ValidationResults ValidateCompanyNameDoesntExist(ValidationResults results, AddCompanyWithOfficeCommand validatableObject)
{
   if (_companyRepository.GetAll().Any(x => x.Name == validatableObject.CompanyName))
   {
        results.AddValidationResult(nameof(validatableObject.CompanyName), "{0} already exists.");
   }
   return results;
}

4.2 Adding the validator to the CommandHandler

Now we are going to add the validator to the commandhandler by modifying the constructor in AddCompanyWithOfficeCommandHandler.cs:

public AddCompanyWithOfficeCommandHandler(IValidator<AddCompanyWithOfficeCommand> validator, ICompanyRepository companyRepository, IOfficeRepository officeRepository) : base(validator)
{
    _companyRepository = companyRepository;
    _officeRepository = officeRepository;
}

Easy

The commandhandlerbase class takes care of calling the validator. So we don’t have to worry about that.

4.3 Registering the validator in the dependency injection

We are going back to the startup.cs to register the validator in dependency injection. Add the following line of code in the method that is called ConfigureServices:

services.AddScoped<IValidator<AddCompanyWithOfficeCommand>, AddCompanyWithOfficeCommandValidator>();

4.4 Testing the validator

Last step is testing the controller. We do this by starting the application and run postman. We are going to create a request with the following URL: “https://localhost:44356/company/AddCompanyWithOfficeCommand”. In the body we will the following json:

{
    "CompanyName": "",
    "CEO": "",
    "BuildingName": "",
    "Street": "",
    "Housenumber": 1,
    "City": ""
}

We now get a result with the following exception:

GJ.CQRSCore.Validation.ValidationException: CompanyName cannot be null or empty
CEO cannot be null or empty
BuildingName cannot be null or empty
Street cannot be null or empty
City cannot be null or empty

If we send the following code twice, the first time we get a succes callback:

{
    "CompanyName": "TestCompany",
    "CEO": "Tester Test",
    "BuildingName": "Test Location",
    "Street": "Test Street",
    "Housenumber": 1,
    "City": "TestCity"
}

The second time we should get the following exception:

GJ.CQRSCore.Validation.ValidationException: CompanyName already exists.
The adress already exists in the database.

5. Conclusion

Now we have a working example in of CQRS. If you want to have a look al the example code. Please click here.

If you have any questions, please let me know!

Leave a Reply

Your email address will not be published. Required fields are marked *