1️⃣ Τι είναι το CQRS;
Το CQRS (Command Query Responsibility Segregation) είναι ένα αρχιτεκτονικό pattern που διαχωρίζει τις λειτουργίες ανάγνωσης (Query) από τις λειτουργίες εγγραφής (Command). Ο σκοπός του είναι να επιτρέπει το σύστημα να διαχειρίζεται τις εγγραφές και τις αναγνώσεις δεδομένων ανεξάρτητα, βελτιώνοντας την απόδοση, την επεκτασιμότητα και τη συντήρηση.
Βασικές Αρχές του CQRS:
✔ Command: Χρησιμοποιείται για την τροποποίηση της κατάστασης του συστήματος (π.χ., δημιουργία, ενημέρωση, διαγραφή).
✔ Query: Χρησιμοποιείται για την ανάκτηση δεδομένων χωρίς να μεταβάλλει την κατάσταση.
✔ Separation of Concerns: Οι αναγνώσεις και οι εγγραφές δεν χρησιμοποιούν το ίδιο data model.
2️⃣ Τι είναι το MediatR Pattern;
Το MediatR είναι μια βιβλιοθήκη στη C# που εφαρμόζει το Mediator Pattern, επιτρέποντας την επικοινωνία μεταξύ των αντικειμένων χωρίς να χρειάζεται να είναι άμεσα συνδεδεμένα. Αυτό προάγει τη χαλαρή σύζευξη (loose coupling) και βελτιώνει τη δομή του κώδικα.
Οφέλη του MediatR:
✅ Απομάκρυνση των άμεσων εξαρτήσεων μεταξύ αντικειμένων.
✅ Καθαρός και δομημένος κώδικας με διαχωρισμό ευθυνών.
✅ Εύκολη επέκταση με νέες λειτουργίες χωρίς να επηρεάζονται άλλα κομμάτια του κώδικα.
3️⃣ Εφαρμογή του CQRS με MediatR στη C#
Βήμα 1: Εγκατάσταση του MediatR
Χρησιμοποιούμε το NuGet package manager για να εγκαταστήσουμε το MediatR:
Install-Package MediatR
Install-Package MediatR.Extensions.Microsoft.DependencyInjection
Βήμα 2: Ορισμός του Command (Εγγραφή/Τροποποίηση Δεδομένων)
Ένα Command χρησιμοποιείται για να εκτελέσει μια ενέργεια που τροποποιεί το σύστημα. Δημιουργούμε ένα request που αντιπροσωπεύει την εντολή.
Παράδειγμα: Δημιουργία χρήστη (CreateUserCommand)
public record CreateUserCommand(string Name, string Email) : IRequest;
Το IRequest σημαίνει ότι αυτή η εντολή επιστρέφει έναν ακέραιο αριθμό (π.χ., το Id του νέου χρήστη).
Βήμα 3: Υλοποίηση του Handler για το Command
public class CreateUserCommandHandler : IRequestHandler
{
private readonly ApplicationDbContext _dbContext;
public CreateUserCommandHandler(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
var user = new User { Name = request.Name, Email = request.Email };
_dbContext.Users.Add(user);
await _dbContext.SaveChangesAsync(cancellationToken);
return user.Id;
}
}
🔹 Ο Handler αναλαμβάνει την εκτέλεση του command και την αλληλεπίδραση με τη βάση δεδομένων.
Βήμα 4: Ορισμός του Query (Ανάγνωση δεδομένων)
Τα Queries χρησιμοποιούνται για την ανάκτηση δεδομένων.
Παράδειγμα: Ανάκτηση χρήστη βάσει ID (GetUserByIdQuery)
public record GetUserByIdQuery(int Id) : IRequest;
Βήμα 5: Υλοποίηση του Handler για το Query
public class GetUserByIdQueryHandler : IRequestHandler
{
private readonly ApplicationDbContext _dbContext;
public GetUserByIdQueryHandler(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
{
return await _dbContext.Users.FindAsync(request.Id);
}
}
🔹 Αυτός ο handler επιστρέφει έναν χρήστη από τη βάση δεδομένων χωρίς να τροποποιεί την κατάσταση του συστήματος.
Βήμα 6: Εγγραφή του MediatR στο Dependency Injection (DI)
Στην κλάση Program.cs, καταχωρούμε το MediatR:
builder.Services.AddMediatR(typeof(Program));
Βήμα 7: Χρήση του MediatR στο Controller
Τώρα μπορούμε να καλέσουμε τις εντολές και τα queries μέσω του MediatR.
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
private readonly IMediator _mediator;
public UsersController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task CreateUser([FromBody] CreateUserCommand command)
{
var userId = await _mediator.Send(command);
return Ok(userId);
}
[HttpGet("{id}")]
public async Task GetUser(int id)
{
var user = await _mediator.Send(new GetUserByIdQuery(id));
return user != null ? Ok(user) : NotFound();
}
}
4️⃣ Πλεονεκτήματα της CQRS & MediatR αρχιτεκτονικής
✅ Χαλαρή σύζευξη (Loose Coupling): Τα components επικοινωνούν μέσω του MediatR, χωρίς να εξαρτώνται άμεσα μεταξύ τους.
✅ Καλύτερη συντηρησιμότητα: Η διαχείριση των εντολών και queries γίνεται με ξεκάθαρη οργάνωση.
✅ Διαχωρισμός ευθυνών (Separation of Concerns): Οι queries και οι commands δεν αναμιγνύονται.
✅ Επεκτασιμότητα: Μπορούμε να προσθέσουμε event handlers, logging, caching χωρίς να αλλάξουμε την υπάρχουσα λογική.
Clean Architecture example using C# to create a simple User Management System. This will cover the following layers:
1. Domain Layer (business rules and entities)
The Domain Layer contains the core logic of the application — the entities (e.g., User) and interfaces (e.g., IUserRepository).
Domain/Entities/User.cs
namespace Domain.Entities
{
public class User
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
}
Domain/Interfaces/IUserRepository.cs
namespace Domain.Interfaces
{
public interface IUserRepository
{
void Add(User user);
User GetById(Guid id);
}
}
2. Application Layer (use cases, DTOs, and commands/queries)
The Application Layer contains the business use cases, including commands, handlers, and DTOs. This is where we define CreateUserCommand, and the CreateUserHandler that processes the command.
Application/Users/Commands/CreateUserCommand.cs
namespace Application.Users.Commands
{
public class CreateUserCommand
{
public string Name { get; set; }
public string Email { get; set; }
}
}
Application/Users/Handlers/CreateUserHandler.cs
using Domain.Entities;
using Domain.Interfaces;
namespace Application.Users.Handlers
{
public class CreateUserHandler
{
private readonly IUserRepository _repository;
public CreateUserHandler(IUserRepository repository)
{
_repository = repository;
}
public void Handle(CreateUserCommand command)
{
var user = new User
{
Id = Guid.NewGuid(),
Name = command.Name,
Email = command.Email
};
_repository.Add(user);
}
}
}
Application/Users/DTOs/CreateUserRequest.cs
namespace Application.Users.DTOs
{
public class CreateUserRequest
{
public string Name { get; set; }
public string Email { get; set; }
}
}
3. Infrastructure Layer (data access implementations)
The Infrastructure Layer contains the actual implementations of interfaces defined in the Domain Layer. Here, we implement IUserRepository to interact with the database or a mock data store.
Infrastructure/Persistence/UserRepository.cs
using Domain.Entities;
using Domain.Interfaces;
using System.Collections.Generic;
using System.Linq;
namespace Infrastructure.Persistence
{
public class UserRepository : IUserRepository
{
private readonly List _users = new();
public void Add(User user)
{
_users.Add(user);
}
public User GetById(Guid id)
{
return _users.FirstOrDefault(u => u.Id == id);
}
}
}
4. Presentation Layer (API controllers or UI)
The Presentation Layer is responsible for interacting with the user. In this case, we’ll use an API Controller to handle HTTP requests. This layer calls the Application Layer (handlers, commands, etc.) to execute the logic.
Presentation/Controllers/UserController.cs
using Application.Users.Commands;
using Application.Users.Handlers;
using Application.Users.DTOs;
using Microsoft.AspNetCore.Mvc;
namespace Presentation.Controllers
{
[ApiController]
[Route("api/users")]
public class UserController : ControllerBase
{
private readonly CreateUserHandler _handler;
public UserController(CreateUserHandler handler)
{
_handler = handler;
}
[HttpPost]
public IActionResult Create([FromBody] CreateUserRequest request)
{
var command = new CreateUserCommand
{
Name = request.Name,
Email = request.Email
};
_handler.Handle(command);
return Ok();
}
}
}
5. Program.cs (Dependency Injection Setup)
In Program.cs, we wire up our dependency injection (DI) so that interfaces and implementations are connected.
Program.cs
using Application.Users.Handlers;
using Domain.Interfaces;
using Infrastructure.Persistence;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddControllers();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseAuthorization();
app.MapControllers();
app.Run();
1. Domain Layer:
- Contains entities (User) and interfaces (IUserRepository).
2. Application Layer:
- Contains use cases (commands like CreateUserCommand and handlers like CreateUserHandler).
- Handles application logic and doesn't depend on external systems like DB or HTTP.
3. Infrastructure Layer:
- Implements interfaces defined in the Domain and Application layers.
- Provides the actual data handling (e.g., UserRepository implements IUserRepository).
4. Presentation Layer:
- Handles user interactions (e.g., API controllers).
- Calls the Application layer to process logic.