ASP.NET Hosting

Creating a CQRS Architecture in .NET Core 8

Effective read and write operation management is essential to software development in order to ensure the scalability and maintainability of applications. This article uses C# and MediatR to demonstrate a real-world implementation of the CQRS (Command Query Responsibility Segregation) architecture. What distinguishes CQRS from alternative architectures? The first one is to concentrate on the procedure rather than the information; allow me to clarify: The CQRS pattern distinguishes between writing activities (commands) and reading operations (questions). We divide our operations according to purpose rather than using a model that works for everyone.

In addition to lowering the coupling between components, MediatR makes it easier to manage handlers in queries and commands. This offers us several benefits, including the ability to keep a cleaner and more organized code and the ability to utilize other technologies or ORM for readings and writing.

This article provides a very basic description of an architecture that includes the aforementioned. It includes the fundamentals: data validation, security, authentication, and mapping. I hope you find it helpful.

We will start with the domain and make our repositories.

DOMAIN

We add a project of type library “Project. Domain” in .NET Core, and we add 1 Entities folder to that folder, which we will create.

BaseEntity.cs

  • Person. cs //table for people
  • User.cs //system users table
public class BaseEntity
{
	public virtual DateTime? CreatedAt { get; set; }
	public virtual string? CreatedBy { get; set; } = string.Empty;
	public virtual DateTime? ModifiedAt { get; set; }
	public virtual string? ModifiedBy { get; set; } =string.Empty;
    [NotMapped]
	public virtual long Id { get; set; }

}
public class Person :BaseEntity
{
    public string Name { get; set; }
	public string LastName { get; set; }
	public DateTime BirthDate { get; set; }
	public string Email { get; set; }
	public string PhoneNumber { get; set; }
}
public partial class User
{
	public long UserId { get; set; }
	public string? Email { get; set; } = string.Empty;
	public string Password { get; set; } = string.Empty;
	public string FirstName { get; set; } = string.Empty;
	public string LastName { get; set; } = string.Empty;
	public string? PhoneNumber { get; set; } = string.Empty;
}

The base entity is an abstraction for the fields common to our tables, it will also help us in creating our generic repository.

The user table handles sensitive information; we do not use it with the base entity.

INFRASTRUCTURE

We create a library-type project “Project. Infrastructure”, and add those references to this project.

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.SqlServer
  • Pomelo.EntityFrameworkCore.MySql

create the folders

  • Migrations (if you are going to work code first, this is not the case)
  • Persistence
  • Context
  • Repositories

In the Context folder, create an AppDbContext.cs file in order to define a custom Entity Framework Core DbContext; in this app part, we manage the database data interactions (CRUD)

in this example, I only have two DbSets Person and User, by another hand in the method “OnModelCreating” I’m using an overridden in order to configure the entity mappings.

In the repositories folder, we will create BaseRepository.cs, PersonRepository.cs, and UserRepository.cs.

Let’s start with BaseRepository, there we will create all the methods to interact with the data referring to its interface IAsyncRepository<T> where T: BaseEntity.

  • GetAllAsync
  • GetAllIquerable
  • AddAsync
  • UpdateAsync
  • DeleteAsync
  • GetByIdAsync
  • AddEntity
  • UpdateEntity
  • DeleteEntity
  • GetAsync (has several overrides for queries, simple, combined, and pagination; usage examples are in the attached file).

PersonRepository

In this part, we use the RepositoryBase<Person> class

public class PersonRepository : RepositoryBase<Person>, IPersonRepository
{
    public PersonRepository(AppDbContext context) : base(context)
    {
    }
}

If you need additional information or any process that is not in the base repository, you can add it here and publish it in your personal interface.

User Repository

There are basic methods to handle users in a small project. If you wish, you could add more stuff like the locked user, unlocked user, levels, etc.

public class UserRepository : IUserRepository
{
    private readonly AppDbContext _context;

    public UserRepository(AppDbContext context)
    {
        _context = context;
    }
    public async Task<long> Insert(User data)
    {
        try
        {
            _context.Set<User>().Add(data);
            await _context.SaveChangesAsync();
            return data.UserId;
        }
        catch (Exception ex)
        {
            throw new Exception(ex.ToString());
        }
    }
    public async Task<User> SelectByIdASync(long userId)
    {
        User obj = await _context.Set<User>().FindAsync(userId);
        return obj;
    }
    public async Task DeleteASync(long userId)
    {
        _context.Set<User>().Remove(await SelectByIdASync(userId));
        await _context.SaveChangesAsync();
    }
    public async Task<User> UpdateASync(long userId, User obj)
    {
        var objPrev = (from iClass in _context.Users
                       where iClass.UserId == userId
                       select iClass).FirstOrDefault();
        _context.Entry(objPrev).State = EntityState.Detached;
        _context.Entry(obj).State = EntityState.Modified;
        await _context.SaveChangesAsync();
        return obj;
    }
    public Task<User> GetByEmail(string email)
    {
        var objPrev = (from iClass in _context.Users
                       where iClass.Email.ToLower() == email.ToLower()
                       select iClass).FirstOrDefaultAsync();
        return objPrev;
    }
}

APPLICATION

We add a project of type library “Project. Application” in netCore, then add the next references to Project. Application.

  • AutoMapper.Extensions.Microsoft.DependencyInjection Version 12.0.1
  • JWT Version 10.1.1
  • MediatR Version 12.4.1
  • Pediatr.Extensions.FluentValidation.AspNetCore Version 5.1.0
  • Pediatr.Extensions.Microsoft.DependencyInjection Version 11.1.0
  • Microsoft.AspNetCore.Cryptography.KeyDerivation Version 8.0.8
  • Microsoft.Extensions.Configuration Version 8.0.0
  • Microsoft.IdentityModel.Tokens Version 8.1.0
  • System.IdentityModel.Tokens.Jwt Version 8.1.0

Please create the next folder path in this project.

Folder path

I’m going to explain the most important classes in this project,

Mapping in order to make the programming work, we use the Mapper package order to map from DTO’s to Class.

public MappingProfile()
{
	CreateMap<PersonRequestDto, Person>();
	CreateMap<Person,PersonRequestDto>();
	CreateMap<Person, PersonListDto>();
}

COMMANDS AND QUERIES

Since this is a CQRS architecture project, this is where the magic happens. The CQRS pattern focuses on processes rather than data. Let me explain: if you have an “employee” entity, you focus on the processes of that entity – creating, deleting, modifying, and querying. We must also remember that this pattern separates reads and writes into different models, using commands to update data and queries to read data. Commands should be task-based rather than data-focused. To achieve this, we use the MediatR library, where we will declare handlers that inherit the properties and methods of IRequestHandler from the MediatR library.

For example, in the person class, we have Hire a Person (Create), Update Person info (Update), Fired Person (Delete), and Get info for Person (Query).

Example code for GetAllPersonHandler

public class GetAllPersonQueryHandler : IRequestHandler<GetAllPersonQuery, List<PersonListDto>>
{
    private readonly IPersonRepository _repository;
    private readonly IMapper _mapper;
    public GetAllPersonQueryHandler(IPersonRepository repository, IMapper mapper)
    {
        _repository = repository;
        _mapper = mapper;
    }
    public async Task<List<PersonListDto>> Handle(GetAllPersonQuery request, CancellationToken cancellationToken)
    {
        var persons = await _repository.GetAllAsync();
        return _mapper.Map<List<PersonListDto>>(persons);
    }
}

This code defines a handler class called GetAllPersonQueryHandler. It handles queries of type GetAllPersonQuery and returns a list of PersonListDto.

The class uses two dependencies: IPersonRepository for accessing data and IMapper for mapping data objects. When the Handle method is called, it fetches all persons from the repository asynchronously.

It then maps the fetched persons to a list of PersonListDto and returns the result.

In the case of a command, such as creating a person, the process is a bit more complex. Besides using mapping to convert from DTO to the person class, we also need to perform validations. Fortunately, we use FluentValidation, which centralizes our validations into a single process and makes programming easier.

VALIDATORS

To implement validations in a CQRMS (Command Query Responsibility Segregation with Microservices) architecture, FluentValidation makes it easy to define rules in a fluid and concise way. In the CreatePersonCommandValidator example, each property of the CreatePersonCommand command is validated individually, ensuring the consistency and accuracy of the data received before being processed. For example, it is established that the first and last names must not be empty and cannot exceed 45 characters. The email property is checked for both its mandatory nature and its correct format, while the birth must be a valid date in the past.

This modular structure not only ensures data integrity but also provides custom error messages, improving the user experience and facilitating code maintenance in complex systems. Another good news is the validator focuses on processes rather than data. Let me explain again; we can create different validators for different business rules and easily find them because they are in the same folder of commands or queries.

Example for Validator in process Creates Person

public class CreatePersonCommandValidator : AbstractValidator<CreatePersonCommand>
{
    public CreatePersonCommandValidator()
    {
        RuleFor(x => x.Person.Name)
            .NotEmpty().WithMessage("The name is required.")
            .MaximumLength(45).WithMessage("The name cannot exceed 45 characters.");
        RuleFor(x => x.Person.LastName)
            .NotEmpty().WithMessage("The last name is required.")
            .MaximumLength(45).WithMessage("The last name cannot exceed 45 characters.");
        RuleFor(x => x.Person.Email)
            .NotEmpty().WithMessage("The email is required.")
            .EmailAddress().WithMessage("Invalid email format.")
            .MaximumLength(45).WithMessage("The email cannot exceed 45 characters.");
        RuleFor(x => x.Person.BirthDate)
            .NotEmpty().WithMessage("The birth date is required.")
            .LessThan(DateTime.Now).WithMessage("Birth date must be in the past.");
    }
}

Example for Result.

SECURITY

For the security of the application, we will use two JWT tools and our password generator, with passwordhasher, we mix the user password to create maximum security encryption and store it in the database, On the other hand, services after the user is correctly identified, this service provides them with a token. With this token, the user can authenticate herself across different system endpoints. Together, these two systems ensure that only the right people can access and use the system securely.

Program. cs

In ASP.NET Core applications, the app’s startup code is located in the Program.cs file. This is where all the necessary services for the application are configured, including database connections, middleware, security, and other essential components. In our program, we will set up the following configuration

  • Database Configuration: A connection to a MySQL database is established using Pomelo lib.
  • Dependency Injection: The different repositories are registered, it is important to indicate that the IPasswordHandler is also registered.
  • MediatR Logging: MediatR is used to handle commands and queries.
  • FluentValidation: A validation behavior is added to the MediatR pipeline to automatically verify data before executing each command or query.
  • Swagger: Swagger is configured at a basic level to have documentation of the APIs that allow us to test easily.
  • JWT: JWT authentication parameters are configured.
  • AutoMapper: to facilitate conversions between models and DTOs in the application.
// Import necessary namespaces for the application
using FluentValidation;
using Serilog;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
// ... (other usings)

// Create the application builder instance
var builder = WebApplication.CreateBuilder(args);

// Configure Database Context
// Add MySQL database context with specific version configuration
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseMySql(
        builder.Configuration.GetConnectionString("DefaultConnection"),
        new MySqlServerVersion(new Version(9, 0, 1))
    )
);

// Register Dependencies
// Configure Dependency Injection for repositories and services
// Base repository pattern implementation
builder.Services.AddScoped(typeof(IAsyncRepository<>), typeof(RepositoryBase<>));
// Specific repositories
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IPersonRepository, PersonRepository>();
// Services
builder.Services.AddScoped<IPasswordHasher, PasswordHasher>();

// Configure Logging
// Set up Serilog for application-wide logging with console output
builder.Host.UseSerilog((context, config) =>
{
    config.WriteTo.Console();
});

// Configure MediatR for CQRS pattern
// Register command and query handlers from different assemblies
builder.Services.AddMediatR(options =>
{
    options.RegisterServicesFromAssembly(typeof(CreateUserCommandHandler).Assembly);
    options.RegisterServicesFromAssembly(typeof(CreatePersonCommandHandler).Assembly);
    options.RegisterServicesFromAssembly(typeof(GetAllPersonQueryHandler).Assembly);
    options.RegisterServicesFromAssembly(typeof(GetListPersonFilteredQueryHandler).Assembly);
});

// Configure Validation
// Register validators from the current assembly
builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
// Register validators from the CreatePersonCommandValidator assembly
builder.Services.AddValidatorsFromAssembly(typeof(CreatePersonCommandValidator).Assembly);
// Add validation pipeline behavior
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));

// Configure Swagger Documentation
// Set up Swagger with JWT authentication support
builder.Services.AddSwaggerGen(options =>
{
    // Basic Swagger information
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Version = "v1",
        Title = "CQRS-example API",
        Description = "API CQRS using MediatR",
    });

    // Configure JWT authentication in Swagger
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,
        Description = "Please insert JWT with Bearer into field",
        Name = "Authorization",
        Type = SecuritySchemeType.ApiKey
    });

    // Add security requirement for JWT
    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        // ... (security requirement configuration)
    });
});

// Configure Controllers and AutoMapper
builder.Services.AddControllers();
builder.Services.AddAutoMapper(typeof(MappingProfile));

// Configure JWT Authentication
// Set up JWT Bearer authentication with custom parameters
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        ValidAudience = builder.Configuration["Jwt:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
    };
});

// Register JWT service
builder.Services.AddSingleton<JwtService>();

// Build the application
var app = builder.Build();

// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
    // Enable Swagger UI in development environment
    app.UseSwagger();
    app.UseSwaggerUI(options =>
    {
        options.SwaggerEndpoint("/swagger/v1/swagger.json", "CQRS API v1");
        options.RoutePrefix = string.Empty;
    });
}

// Configure middleware pipeline
app.UseAuthentication();    // Enable authentication
app.UseAuthorization();     // Enable authorization
app.MapControllers();       // Map controller endpoints
app.UseErrorHandlingMiddleware();  // Add custom error handling

// Start the application
app.Run();

As an additional item, I leave some examples of how to use the generic Repository for simple and complex queries.

BASICS QUERIES

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int Stock { get; set; }
    public DateTime CreatedDate { get; set; }
}

Example 1. Filter products with a price greater than a specific value.

var expensiveProducts = await _repository.GetAsync(
    p => p.Price > 100
);

Example 2. Filter products with a specific name.

var namedProducts = await _repository.GetAsync(
    p => p.Name == "Laptop"
);

Example 3. Filter products with stock greater than 50.

var inStockProducts = await _repository.GetAsync(
    p => p.Stock > 50
);

Example 4. Filter products created in the last month.

var recentProducts = await _repository.GetAsync(
    p => p.CreatedDate >= DateTime.Now.AddMonths(-1)
);

Example 5. Filter products whose name contains a specific word.

var filteredProducts = await _repository.GetAsync(
    p => p.Name.Contains("Pro")
);

Example 6. Filter products whose price is within a specific range.

var midRangeProducts = await _repository.GetAsync(
    p => p.Price >= 50 && p.Price <= 150
);

Example 7. Filter products by multiple conditions (for example, a specific name and price that is less than a value).

var specificProducts = await _repository.GetAsync(
    p => p.Name == "Phone" && p.Price < 200
);
Example 8. Filter products with stock between 10 and 100 units.
var rangeStockProducts = await _repository.GetAsync(
    p => p.Stock >= 10 && p.Stock <= 100
);

WITH ORDER BY PARAMETER

Example 1. Sort products by name alphabetically.

var productsOrderedByName = await _repository.GetAsync(
    orderBy: q => q.OrderBy(p => p.Name)
);

Example 2. Sort products by price from highest to lowest.

var productsOrderedByPriceDesc = await _repository.GetAsync(
    orderBy: q => q.OrderByDescending(p => p.Price)
);

Example 3. Sort products by creation date and then by price.

var productsOrderedByDateAndPrice = await _repository.GetAsync(
    orderBy: q => q
        .OrderBy(p => p.CreatedDate)
        .ThenBy(p => p.Price)
);

Example 8. Filter products with stock between 10 and 100 units.

var rangeStockProducts = await _repository.GetAsync(
    p => p.Stock >= 10 && p.Stock <= 100
);

INCLUDE STRING: INCLUDE RELATIONSHIPS BETWEEN ENTITIES

The includeString parameter is used to include related entities in the query, that is, to load data from related entities (known as “eager loading”). This is useful when you need to access related data in the same query to avoid multiple trips to the database.

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int Stock { get; set; }
    public DateTime CreatedDate { get; set; }
    public int CategoryId { get; set; }
    public Category Category { get; set; }
}
public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Example 1. Include the Category entity in the query.

var productsWithCategory = await _repository.GetAsync(
    includeString: "Category"
);

Example 2. Include a nested relationship (for example, if Category had a relationship with another entity).

var productsWithCategoryAndDetails = await _repository.GetAsync(
    includeString: "Category.Details"
);

Example 3. Bring the product list (id, name, price) of their respective categories (category. id).

var productsWithCategory = await _repository.GetAsync(
    includeString: "Category"
).Select(p => new
{
    ProductId = p.Id,
    ProductName = p.Name,
    ProductPrice = p.Price,
    CategoryId = p.Category.Id
}).ToListAsync();

MORE COMPLETE METHODS INCLUDE

Example 1. Include the Category entity.

var productsWithCategory = await _repository.GetAsync(
    includes: new List<Expression<Func<Product, object>>>
    {
        p => p.Category
    }
);

Example 2. Include multiple related entities.

If the Product has another related entity, for example, a Supplier, you can include both relationships.

var productsWithCategoryAndSupplier = await _repository.GetAsync(
    includes: new List<Expression<Func<Product, object>>>
    {
        p => p.Category,
        p => p.Supplier
    }
);

Example 3. Using orderBy and predicate together with includes.

var filteredAndOrderedProducts = await _repository.GetAsync(
    predicate: p => p.Price > 100,
    orderBy: q => q.OrderBy(p => p.Name),
    includes: new List<Expression<Func<Product, object>>>
    {
        p => p.Category
    }
);

Example 4. showing the id, name, and price from the product table and the name from the Category table.

var filteredAndOrderedProducts = await _repository.GetAsync(
    predicate: p => p.Price > 100,
    orderBy: q => q.OrderBy(p => p.Name),
    includes: new List<Expression<Func<Product, object>>> { p => p.Category }
)
.Select(p => new
{
    ProductId = p.Id,
    ProductName = p.Name,
    ProductPrice = p.Price,
    CategoryName = p.Category.Name
})
.ToListAsync();

USING PAGINATION

int pageNumber = 1;
int pageSize = 10;

var paginatedProducts = await _repository.GetAsync(
    predicate: p => p.Price > 100,
    orderBy: q => q.OrderBy(p => p.Name),
    includes: new List<Expression<Func<Product, object>>> { p => p.Category },
    skip: (pageNumber - 1) * pageSize,
    take: pageSize
).Select(p => new
{
    ProductId = p.Id,
    ProductName = p.Name,
    ProductPrice = p.Price,
    CategoryName = p.Category.Name
}).ToListAsync();

example

var query = from en in context.Entities
            join ti in context.TableIndices
            on new { DocumentTypeId = en.DocumentTypeId.ToString(), Module = "Entity", Field = "DocumentType" }
            equals new { DocumentTypeId = ti.StringValue, ti.Module, ti.Field }
            into joinedData
            from you in joinedData.DefaultIfEmpty()
            select new
            {
                en.Id,
                en.SocialReason,
                en.DocumentTypeId,
                Description = you.Description,
                en.DocumentNumber
            };
var result = query.ToList();

ASP.NET Core 9 Hosting Recommendation

HostForLIFE.eu
HostForLIFE.eu is a popular recommendation that offers various hosting choices. Starting from shared hosting to dedicated servers, you will find options fit for beginners and popular websites. It offers various hosting choices if you want to scale up. Also, you get flexible billing plans where you can choose to purchase a subscription even for one or six months.