Core Concepts - PogovorovDaniil/Requestum GitHub Wiki

Core Concepts

Understanding the core concepts of Requestum is essential for building clean, maintainable applications. This guide explains the fundamental building blocks of the library.

🎯 The CQRS Pattern

Requestum is built around the Command Query Responsibility Segregation (CQRS) pattern, which separates operations into three distinct categories:

  1. Commands - Operations that modify state
  2. Queries - Operations that retrieve data
  3. Events - Notifications about state changes

This separation provides:

  • Clarity - Intent is clear from the interface used
  • Maintainability - Easier to understand and modify code
  • Scalability - Different scaling strategies for reads and writes
  • Testability - Simpler unit tests with focused responsibilities

📦 Request Types

ICommand

Commands represent actions that change the system state. They do not return values.

public interface ICommand : IBaseRequest;

Key Characteristics:

  • Represents an action (imperative naming: CreateUser, DeleteOrder)
  • Modifies system state
  • Does not return a value
  • Has exactly one handler
  • Use when you need to change something

Example:

public record CreateUserCommand(string Name, string Email) : ICommand;
public record DeleteOrderCommand(int OrderId) : ICommand;
public record UpdateProductPriceCommand(int ProductId, decimal NewPrice) : ICommand;

IQuery

Queries represent requests for data. They do not modify state and return typed responses.

public interface IQuery<TResponse> : IBaseRequest;

Key Characteristics:

  • Represents a question (descriptive naming: GetUser, SearchProducts)
  • Does not modify state
  • Returns a typed response
  • Has exactly one handler
  • Use when you need to read something

Example:

public record GetUserQuery(int UserId) : IQuery<UserDto>;
public record SearchProductsQuery(string SearchTerm) : IQuery<List<ProductDto>>;
public record GetOrderTotalQuery(int OrderId) : IQuery<decimal>;

IEventMessage

Events represent notifications about something that has happened in the system.

public interface IEventMessage : IBaseRequest;

Key Characteristics:

  • Represents a fact (past tense naming: UserCreated, OrderPlaced)
  • Notification of a state change that already occurred
  • Does not return a value
  • Can have zero or more receivers
  • Use when you need to notify about something

Example:

public record UserCreatedEvent(int UserId, string Name, string Email) : IEventMessage;
public record OrderPlacedEvent(int OrderId, decimal Total) : IEventMessage;
public record ProductPriceChangedEvent(int ProductId, decimal OldPrice, decimal NewPrice) : IEventMessage;

🔧 Handler Types

Command Handlers

Command handlers execute commands and modify system state.

Synchronous:

public interface ICommandHandler<TCommand> : IBaseHandler<TCommand>
    where TCommand : ICommand
{
    void Execute(TCommand command);
}

Asynchronous:

public interface IAsyncCommandHandler<TCommand> : IBaseHandler<TCommand>
    where TCommand : ICommand
{
    Task ExecuteAsync(TCommand command, CancellationToken cancellationToken = default);
}

When to use:

  • Sync: Simple in-memory operations, validation
  • Async: Database operations, API calls, file I/O

Query Handlers

Query handlers process queries and return data.

Synchronous:

public interface IQueryHandler<TQuery, TResponse> : IBaseHandler<TQuery>
    where TQuery : IQuery<TResponse>
{
    TResponse Handle(TQuery query);
}

Asynchronous:

public interface IAsyncQueryHandler<TQuery, TResponse> : IBaseHandler<TQuery>
  where TQuery : IQuery<TResponse>
{
    Task<TResponse> HandleAsync(TQuery query, CancellationToken cancellationToken = default);
}

When to use:

  • Sync: In-memory calculations, cached data access
  • Async: Database queries, external API calls

Event Receivers

Event receivers respond to published events.

Synchronous:

public interface IEventMessageReceiver<TMessage> : IBaseHandler<TMessage>
    where TMessage : IEventMessage
{
    void Receive(TMessage message);
}

Asynchronous:

public interface IAsyncEventMessageReceiver<TMessage> : IBaseHandler<TMessage>
    where TMessage : IEventMessage
{
    Task ReceiveAsync(TMessage message, CancellationToken cancellationToken = default);
}

Key Differences from Handlers:

  • Multiple receivers allowed per event type
  • Fire-and-forget semantics
  • Decoupled from the event publisher

🔄 Execution Flow

Command Execution Flow

Client Code
  ↓
IRequestum.ExecuteAsync()
  ↓
Middleware Pipeline (if configured)
  ↓
IAsyncCommandHandler.ExecuteAsync()
  ↓
Database/External Services

Query Execution Flow

Client Code
  ↓
IRequestum.HandleAsync()
  ↓
Middleware Pipeline (if configured)
  ↓
IAsyncQueryHandler.HandleAsync()
  ↓
Database/External Services
  ↓
Return TResponse

Event Publishing Flow

Client Code
  ↓
IRequestum.PublishAsync()
  ↓
All IAsyncEventMessageReceiver.ReceiveAsync()
  ↓
Each receiver processes independently

🎨 Naming Conventions

Following consistent naming conventions makes your code more readable and maintainable.

Commands

Use imperative verbs (actions):

  • CreateUser
  • UpdateOrder
  • DeleteProduct
  • SendEmail
  • CalculateTotal

Queries

Use descriptive questions:

  • GetUserById
  • SearchProducts
  • ListOrders
  • FindCustomer
  • CalculateDiscount

Events

Use past tense (facts):

  • UserCreated
  • OrderUpdated
  • ProductDeleted
  • EmailSent
  • PaymentProcessed

🧩 Request vs Response

Commands

Commands typically don't return values, but if you need to return something (like an ID), consider using events instead:

// ❌ Avoid this
public record CreateUserCommand : IQuery<int>; // Returns user ID

// ✅ Better approach
public record CreateUserCommand(string Name, string Email) : ICommand;
public record UserCreatedEvent(int UserId, string Name, string Email) : IEventMessage;

// In handler:
public async Task ExecuteAsync(CreateUserCommand command, CancellationToken ct)
{
    var user = await _repository.CreateAsync(command, ct);
    await _requestum.PublishAsync(new UserCreatedEvent(user.Id, user.Name, user.Email), ct);
}

Queries

Always return a specific response type:

// ✅ Good - specific response
public record GetUserQuery(int Id) : IQuery<UserDto>;

// ✅ Good - collection response
public record SearchUsersQuery(string Term) : IQuery<List<UserDto>>;

// ❌ Avoid - too generic
public record GetDataQuery(string Type) : IQuery<object>;

🔑 Key Principles

1. Single Responsibility

Each handler should do one thing well:

// ✅ Good - focused responsibility
public class CreateUserHandler : IAsyncCommandHandler<CreateUserCommand>
{
    public async Task ExecuteAsync(CreateUserCommand command, CancellationToken ct)
    {
        // Only creates user
        await _repository.AddAsync(new User(command.Name, command.Email), ct);
    }
}

// ❌ Bad - multiple responsibilities
public class CreateUserHandler : IAsyncCommandHandler<CreateUserCommand>
{
    public async Task ExecuteAsync(CreateUserCommand command, CancellationToken ct)
    {
        // Creates user
        var user = await _repository.AddAsync(new User(command.Name, command.Email), ct);
        
        // Sends email (should be in event receiver)
        await _emailService.SendWelcomeEmail(user.Email);
     
        // Updates analytics (should be in event receiver)
        await _analytics.TrackSignup(user.Id);
    }
}

2. Separation of Concerns

Keep commands, queries, and events separate:

// ✅ Good - clear separation
public record CreateUserCommand(string Name, string Email) : ICommand;
public record GetUserQuery(int Id) : IQuery<UserDto>;
public record UserCreatedEvent(int UserId, string Name) : IEventMessage;

// ❌ Bad - mixed concerns
public record UserCommand(int? Id, string Name, string Email, bool IsQuery) : ICommand;

3. Immutability

Use records or immutable classes for requests:

// ✅ Good - immutable record
public record CreateUserCommand(string Name, string Email) : ICommand;

// ✅ Good - immutable class
public class CreateUserCommand : ICommand
{
    public required string Name { get; init; }
    public required string Email { get; init; }
}

// ❌ Bad - mutable properties
public class CreateUserCommand : ICommand
{
    public string Name { get; set; }
    public string Email { get; set; }
}

🎓 Next Steps

Now that you understand the core concepts, explore:

  • Commands - Detailed guide on working with commands
  • Queries - Detailed guide on working with queries
  • Events - Detailed guide on working with events
  • Middleware Pipeline - Building cross-cutting concerns

← Back to Home | Commands →

⚠️ **GitHub.com Fallback** ⚠️