| .. | ||
| commands | ||
| dynamic-queries | ||
| queries | ||
| validation | ||
| README.md | ||
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:
- Commands Overview - Introduction to commands
- Basic Commands - Commands without results
- Commands with Results - Commands that return values
- Command Registration - Registration patterns
- Command Authorization - ICommandAuthorizationService
- Command Attributes - [IgnoreCommand], [CommandName], etc.
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:
- Queries Overview - Introduction to queries
- Basic Queries - Simple query handlers
- Query Registration - Registration patterns
- Query Authorization - IQueryAuthorizationService
- Query Attributes - [IgnoreQuery], [QueryName], etc.
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:
- Validation Overview - Introduction to validation
- FluentValidation Setup - Setting up validators
- HTTP Validation - RFC 7807 Problem Details
- gRPC Validation - Google Rich Error Model
- Custom Validation - Custom validation scenarios
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:
- Dynamic Queries Overview - Introduction to dynamic queries
- Getting Started - First dynamic query
- Filters and Sorts - Filtering, sorting, paging
- Groups and Aggregates - Grouping and aggregation
- Queryable Providers - IQueryableProvider implementation
- Alter Queryable Services - Security filters, tenant isolation
- Interceptors - IDynamicQueryInterceptorProvider
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:
- Commands - Master write operations
- Queries - Master read operations
- Validation - Add input validation
- Dynamic Queries - Advanced querying
See Also
- Getting Started - Build your first application
- Architecture - Understanding the framework design
- Best Practices - Production-ready patterns
- Tutorials - Comprehensive examples