ASP.NET Hosting

How CQRS and MediatR Enhance ASP.NET Core Web API Architecture?

As applications evolve from simple CRUD systems into large, business-critical platforms, maintaining clean architecture becomes increasingly challenging. Over time, developers often face bloated controllers, tightly coupled services, and domain models that try to handle both read and write operations. This complexity makes systems difficult to maintain, test, and scale.

CQRS (Command Query Responsibility Segregation), combined with MediatR, offers a structured and scalable solution to these problems. By separating read and write responsibilities and introducing a mediator to decouple components, CQRS enables cleaner code, better separation of concerns, and improved long-term maintainability.

In this article, we’ll explore:

  • What CQRS is and why it matters
  • When CQRS should be used
  • How MediatR supports CQRS
  • A step-by-step implementation in ASP.NET Core Web API

Best practices and architectural trade-offs

What Is CQRS?

CQRS stands for Command Query Responsibility Segregation.
It is a design pattern that separates read operations (queries) from write operations (commands).

Core Principle

  • Commands change application state (Create, Update, Delete)
  • Queries retrieve data and never modify state

In traditional CRUD applications, the same model is often used for both reading and writing. While this works for small systems, it quickly becomes problematic as complexity grows.

Why CQRS Exists

Using a single model for both reads and writes often leads to:

  • Overloaded domain models
  • Complex DTO mappings
  • Security risks due to over-exposed data
  • Limited performance optimization

CQRS allows each side to evolve independently, improving clarity, security, and scalability.

When Should You Use CQRS?

CQRS is not a silver bullet and should not be used everywhere.

CQRS Is a Good Fit When:

  • Business logic is complex
  • Read and write workloads differ significantly
  • The system requires strong validation and workflows
  • Multiple teams work on the same domain
  • Long-term scalability is important

Avoid CQRS When:

  • The application is small or short-lived
  • CRUD operations are straightforward
  • Simplicity is more important than flexibility

Good rule: If your application is simple today but guaranteed to grow, CQRS is worth considering.

What Is MediatR?

MediatR is a lightweight .NET library that implements the Mediator pattern.

Instead of components calling each other directly, they communicate through a mediator. This eliminates tight coupling and improves testability.

Benefits of MediatR

  • Decouples controllers from business logic
  • Encourages single-responsibility handlers
  • Simplifies cross-cutting concerns (logging, validation)
  • Improves maintainability and readability

MediatR works in-process, making it ideal for clean application architectures.

CQRS and MediatR Together

CQRS defines what operations exist (commands and queries).
MediatR defines how those operations are dispatched and handled.

CQRS Concept MediatR Component
Command IRequest
Query IRequest<TResponse>
Command Handler IRequestHandler
Query Handler IRequestHandler
Domain Event INotification
Cross-cutting logic IPipelineBehavior

Recommended Project Structure

├── Controllers

├── Commands

├── Queries

├── Handlers

├── Models

├── DTOs

├── Data

├── Repositories

This structure:

  • Keeps responsibilities isolated
  • Scales well as the application grows
  • Aligns with Clean Architecture principles

High-Level Implementation Flow

  1. Controller receives HTTP request
  2. Command or Query is created
  3. MediatR dispatches the request
  4. Handler executes business logic
  5. Repository/Data layer persists or fetches data
  6. Response is returned to the controller

Controllers remain thin, and business logic lives in handlers.

Advanced MediatR Capabilities

Notifications (Domain Events)

Notifications allow multiple handlers to react to the same event.

Common use cases:

  • Sending emails
  • Invalidating caches
  • Publishing integration events

This enables event-driven behavior without tight coupling.

Pipeline Behaviors

Pipeline behaviors act like middleware for MediatR requests.

Typical scenarios:

  • Validation (FluentValidation)
  • Logging
  • Authorization
  • Performance tracking

They provide a clean way to handle cross-cutting concerns without duplicating code.

CQRS Trade-Offs

Advantages

  • Clear separation of concerns
  • Highly testable architecture
  • Easier long-term maintenance
  • Scales well with complexity

Challenges

  • More files and abstractions
  • Steeper learning curve
  • Overkill for simple CRUD apps

CQRS is an architectural investment, not a shortcut.

CQRS in Microservices

CQRS fits naturally into microservices architectures.

  • MediatR handles in-process CQRS
  • Message brokers (Kafka, Azure Service Bus) handle cross-service events
  • Commands and queries can evolve independently
  • Event-driven systems become easier to manage

CQRS becomes especially powerful when combined with event-based communication.

A step-by-step implementation in ASP.NET Core Web API

Model (Domain Entity) – Models/Project.cs

namespace ProjectDemo.Models
{
    public class Project
    {
        public int Id { get; set; }

        public string Name { get; set; } = null!;

        public string Description { get; set; } = null!;

        public DateTime CreatedOn { get; set; }
    }
}

DTO (Request & Response) – DTO/CreateProjectRequest.cs ,DTOs/ProjectResponse.cs

namespace ProjectDemo.DTOs
{
    public class CreateProjectRequest
    {
        public string Name { get; set; } = null!;
        public string Description { get; set; } = null!;
    }
}
namespace ProjectDemo.DTOs
{
    public class ProjectResponse
    {
        public int Id { get; set; }

        public string Name { get; set; } = null!;

        public string Description { get; set; } = null!;
    }
}

Database Context – Data/AppDbContext.cs

using Microsoft.EntityFrameworkCore;
using ProjectDemo.Models;

namespace ProjectDemo.Data
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options)
            : base(options) { }

        public DbSet<Project> Projects => Set<Project>();
    }
}

Repository Layer

IProjectRepository.cs ,

using ProjectDemo.Models;

namespace ProjectDemo.Repositories
{
    public interface IProjectRepository
    {
        Task<Project> AddAsync(Project project);
    }
}

ProjectRepository.cs

using ProjectDemo.Data;
using ProjectDemo.Models;

namespace ProjectDemo.Repositories
{
    public class ProjectRepository : IProjectRepository
    {
        private readonly AppDbContext _context;

        public ProjectRepository(AppDbContext context)
        {
            _context = context;
        }

        public async Task<Project> AddAsync(Project project)
        {
            _context.Projects.Add(project);
            await _context.SaveChangesAsync();
            return project;
        }
    }
}

Command (Write Operation) – Commands/CreateProjectCommand.cs

using MediatR;
using ProjectDemo.DTOs;

namespace ProjectDemo.Commands
{
    public class CreateProjectCommand : IRequest<ProjectResponse>
    {
        public string Name { get; set; } = null!;
        public string Description { get; set; } = null!;
    }
}

Command Handler – Handlers/CreateProjectHandler.cs

using MediatR;
using ProjectDemo.Commands;
using ProjectDemo.DTOs;
using ProjectDemo.Models;
using ProjectDemo.Repositories;

namespace ProjectDemo.Handlers
{
    public class CreateProjectHandler
        : IRequestHandler<CreateProjectCommand, ProjectResponse>
    {
        private readonly IProjectRepository _repository;

        public CreateProjectHandler(IProjectRepository repository)
        {
            _repository = repository;
        }

        public async Task<ProjectResponse> Handle(
            CreateProjectCommand command,
            CancellationToken cancellationToken)
        {
            var project = new Project
            {
                Name = command.Name,
                Description = command.Description,
                CreatedOn = DateTime.UtcNow
            };

            var result = await _repository.AddAsync(project);

            return new ProjectResponse
            {
                Id = result.Id,
                Name = result.Name,
                Description = result.Description
            };
        }
    }
}

Controller (API Endpoint) – Controllers/ProjectsController.cs

using MediatR;
using Microsoft.AspNetCore.Mvc;
using ProjectDemo.Commands;
using ProjectDemo.DTOs;

namespace ProjectDemo.Controllers
{
    [ApiController]
    [Route("api/projects")]
    public class ProjectsController : ControllerBase
    {
        private readonly IMediator _mediator;

        public ProjectsController(IMediator mediator)
        {
            _mediator = mediator;
        }

        [HttpPost]
        public async Task<IActionResult> CreateProject(
            [FromBody] CreateProjectRequest request)
        {
            var command = new CreateProjectCommand
            {
                Name = request.Name,
                Description = request.Description
            };

            var response = await _mediator.Send(command);

            return Ok(response);
        }
    }
}

Enable CORS (IMPORTANT) – Program.cs

using MediatR;
using Microsoft.EntityFrameworkCore;
using ProjectDemo.Data;
using ProjectDemo.Repositories;
using System.Reflection;

var builder = WebApplication.CreateBuilder(args);

// Add CORS
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowFrontend",
        policy =>
        {
            policy.AllowAnyOrigin()
                  .AllowAnyMethod()
                  .AllowAnyHeader();
        });
});

// Database
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("DefaultConnection")));

// MediatR
builder.Services.AddMediatR(cfg =>
    cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));

// Repositories
builder.Services.AddScoped<IProjectRepository, ProjectRepository>();

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Use CORS
app.UseCors("AllowFrontend");

app.UseSwagger();
app.UseSwaggerUI();

app.UseAuthorization();

app.MapControllers();

app.Run();

appsettings.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=.;Database=ProjectDb;Trusted_Connection=True;"
  }
}

Request /Response Example (POST)

Request

{
  "name": "Inventory System",
  "description": "Microservice-based inventory management"
}

Response

{
  "id": 1,
  "name": "Inventory System",
  "description": "Microservice-based inventory management"
}

Conclusion

CQRS combined with MediatR provides a clean, scalable, and maintainable architecture for modern ASP.NET Core Web APIs. By separating read and write concerns and introducing a mediator, applications become easier to evolve, test, and reason about.

While CQRS should not be applied prematurely, it becomes invaluable as systems grow in complexity. When used thoughtfully, it lays a strong foundation for enterprise-grade, future-proof applications.

ASP.NET Core 10.0 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.