dotnet-cqrs/docs/core-features
2025-12-11 01:18:24 -05:00
..
commands this is a mess 2025-12-11 01:18:24 -05:00
dynamic-queries this is a mess 2025-12-11 01:18:24 -05:00
queries this is a mess 2025-12-11 01:18:24 -05:00
validation this is a mess 2025-12-11 01:18:24 -05:00
README.md this is a mess 2025-12-11 01:18:24 -05:00

Core Features

Master the fundamental features of Svrnty.CQRS: commands, queries, validation, and dynamic queries.

Overview

Svrnty.CQRS provides four core feature sets:

  • Commands - Write operations that change system state
  • Queries - Read operations that retrieve data
  • Validation - Input validation with FluentValidation
  • Dynamic Queries - OData-like filtering, sorting, and aggregation

Feature Categories

Commands

Commands represent write operations:

Quick example:

public record CreateUserCommand
{
    public string Name { get; init; } = string.Empty;
    public string Email { get; init; } = string.Empty;
}

public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, int>
{
    public async Task<int> HandleAsync(CreateUserCommand command, CancellationToken cancellationToken)
    {
        // Create user logic
        return userId;
    }
}

// Registration
builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();

// Endpoint: POST /api/command/createUser

Queries

Queries represent read operations:

Quick example:

public record GetUserQuery
{
    public int UserId { get; init; }
}

public class GetUserQueryHandler : IQueryHandler<GetUserQuery, UserDto>
{
    public async Task<UserDto> HandleAsync(GetUserQuery query, CancellationToken cancellationToken)
    {
        // Fetch user data
        return userDto;
    }
}

// Registration
builder.Services.AddQuery<GetUserQuery, UserDto, GetUserQueryHandler>();

// Endpoints:
//   GET /api/query/getUser?userId=1
//   POST /api/query/getUser

Validation

Input validation with FluentValidation:

Quick example:

public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
    public CreateUserCommandValidator()
    {
        RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
        RuleFor(x => x.Email).NotEmpty().EmailAddress();
    }
}

// Registration with validator
builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler, CreateUserCommandValidator>();

Dynamic Queries

OData-like querying capabilities:

Quick example:

// Provider
public class UserQueryableProvider : IQueryableProvider<User>
{
    private readonly ApplicationDbContext _context;

    public Task<IQueryable<User>> GetQueryableAsync(object query, CancellationToken cancellationToken)
    {
        return Task.FromResult(_context.Users.AsQueryable());
    }
}

// Registration
builder.Services.AddDynamicQueryWithProvider<User, UserQueryableProvider>();

// Endpoint: POST /api/query/users
// Request body:
{
  "filters": [{ "path": "Name", "type": 2, "value": "Alice" }],
  "sorts": [{ "path": "Email", "ascending": true }],
  "page": 1,
  "pageSize": 10
}

Feature Comparison

Feature Commands Queries Dynamic Queries
Purpose Write data Read data Advanced read with filters
Returns data Optional Always Always
HTTP methods POST only GET or POST GET or POST
Caching No Yes Yes
Side effects Yes No No
Validation Yes Yes Yes
Filtering N/A Manual Automatic
Sorting N/A Manual Automatic
Paging N/A Manual Automatic

Core Interfaces

Command Interfaces

// Command without result
public interface ICommandHandler<in TCommand>
{
    Task HandleAsync(TCommand command, CancellationToken cancellationToken = default);
}

// Command with result
public interface ICommandHandler<in TCommand, TResult>
{
    Task<TResult> HandleAsync(TCommand command, CancellationToken cancellationToken = default);
}

Query Interface

// Query always returns result
public interface IQueryHandler<in TQuery, TResult>
{
    Task<TResult> HandleAsync(TQuery query, CancellationToken cancellationToken = default);
}

Dynamic Query Interfaces

// Dynamic query interface
public interface IDynamicQuery<TSource, TDestination>
{
    List<IFilter> GetFilters();
    List<ISort> GetSorts();
    List<IGroup> GetGroups();
    List<IAggregate> GetAggregates();
    int? Page { get; }
    int? PageSize { get; }
}

// Queryable provider
public interface IQueryableProvider<TSource>
{
    Task<IQueryable<TSource>> GetQueryableAsync(object query, CancellationToken cancellationToken = default);
}

Quick Start Examples

Simple CRUD Operations

// Create
public record CreateProductCommand
{
    public string Name { get; init; } = string.Empty;
    public decimal Price { get; init; }
}

public class CreateProductCommandHandler : ICommandHandler<CreateProductCommand, int>
{
    private readonly ApplicationDbContext _context;

    public async Task<int> HandleAsync(CreateProductCommand command, CancellationToken cancellationToken)
    {
        var product = new Product { Name = command.Name, Price = command.Price };
        _context.Products.Add(product);
        await _context.SaveChangesAsync(cancellationToken);
        return product.Id;
    }
}

// Read
public record GetProductQuery
{
    public int ProductId { get; init; }
}

public class GetProductQueryHandler : IQueryHandler<GetProductQuery, ProductDto>
{
    private readonly ApplicationDbContext _context;

    public async Task<ProductDto> HandleAsync(GetProductQuery query, CancellationToken cancellationToken)
    {
        var product = await _context.Products.FindAsync(new object[] { query.ProductId }, cancellationToken);
        return new ProductDto { Id = product.Id, Name = product.Name, Price = product.Price };
    }
}

// Update
public record UpdateProductCommand
{
    public int ProductId { get; init; }
    public string Name { get; init; } = string.Empty;
    public decimal Price { get; init; }
}

public class UpdateProductCommandHandler : ICommandHandler<UpdateProductCommand>
{
    private readonly ApplicationDbContext _context;

    public async Task HandleAsync(UpdateProductCommand command, CancellationToken cancellationToken)
    {
        var product = await _context.Products.FindAsync(new object[] { command.ProductId }, cancellationToken);
        product.Name = command.Name;
        product.Price = command.Price;
        await _context.SaveChangesAsync(cancellationToken);
    }
}

// Delete
public record DeleteProductCommand
{
    public int ProductId { get; init; }
}

public class DeleteProductCommandHandler : ICommandHandler<DeleteProductCommand>
{
    private readonly ApplicationDbContext _context;

    public async Task HandleAsync(DeleteProductCommand command, CancellationToken cancellationToken)
    {
        var product = await _context.Products.FindAsync(new object[] { command.ProductId }, cancellationToken);
        _context.Products.Remove(product);
        await _context.SaveChangesAsync(cancellationToken);
    }
}

// Registration
builder.Services.AddCommand<CreateProductCommand, int, CreateProductCommandHandler>();
builder.Services.AddQuery<GetProductQuery, ProductDto, GetProductQueryHandler>();
builder.Services.AddCommand<UpdateProductCommand, UpdateProductCommandHandler>();
builder.Services.AddCommand<DeleteProductCommand, DeleteProductCommandHandler>();

Common Patterns

Pattern 1: Command with Validation

// Command
public record CreateUserCommand
{
    public string Name { get; init; } = string.Empty;
    public string Email { get; init; } = string.Empty;
    public int Age { get; init; }
}

// Validator
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
    public CreateUserCommandValidator()
    {
        RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
        RuleFor(x => x.Email).NotEmpty().EmailAddress();
        RuleFor(x => x.Age).GreaterThan(0).LessThanOrEqualTo(120);
    }
}

// Handler
public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, int>
{
    public async Task<int> HandleAsync(CreateUserCommand command, CancellationToken cancellationToken)
    {
        // Validation already ran before handler execution
        // Business logic here
    }
}

Pattern 2: Query with Authorization

// Query
public record GetUserQuery
{
    public int UserId { get; init; }
}

// Authorization
public class GetUserAuthorizationService : IQueryAuthorizationService<GetUserQuery>
{
    public async Task<bool> CanExecuteAsync(GetUserQuery query, ClaimsPrincipal user, CancellationToken cancellationToken)
    {
        // Users can only view their own data (or admins)
        if (user.IsInRole("Admin"))
            return true;

        var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        return query.UserId.ToString() == userId;
    }
}

// Handler
public class GetUserQueryHandler : IQueryHandler<GetUserQuery, UserDto>
{
    public async Task<UserDto> HandleAsync(GetUserQuery query, CancellationToken cancellationToken)
    {
        // Authorization already checked before handler execution
        // Fetch logic here
    }
}

Pattern 3: Dynamic Query with Security Filter

// Queryable provider
public class OrderQueryableProvider : IQueryableProvider<Order>
{
    private readonly ApplicationDbContext _context;

    public Task<IQueryable<Order>> GetQueryableAsync(object query, CancellationToken cancellationToken)
    {
        return Task.FromResult(_context.Orders.AsQueryable());
    }
}

// Security filter
public class OrderSecurityFilter : IAlterQueryableService<Order, OrderDto>
{
    public IQueryable<Order> AlterQueryable(IQueryable<Order> queryable, object query, ClaimsPrincipal user)
    {
        // Non-admins can only see their own orders
        if (user.IsInRole("Admin"))
            return queryable;

        var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        return queryable.Where(o => o.UserId.ToString() == userId);
    }
}

// Registration
builder.Services.AddDynamicQueryWithProvider<Order, OrderQueryableProvider>();
builder.Services.AddScoped<IAlterQueryableService<Order, OrderDto>, OrderSecurityFilter>();

What's Next?

Dive deep into each feature:

  1. Commands - Master write operations
  2. Queries - Master read operations
  3. Validation - Add input validation
  4. Dynamic Queries - Advanced querying

See Also