dotnet-cqrs/docs/core-features/README.md

415 lines
12 KiB
Markdown

# 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/README.md)
Commands represent write operations:
- [Commands Overview](commands/README.md) - Introduction to commands
- [Basic Commands](commands/basic-commands.md) - Commands without results
- [Commands with Results](commands/commands-with-results.md) - Commands that return values
- [Command Registration](commands/command-registration.md) - Registration patterns
- [Command Authorization](commands/command-authorization.md) - ICommandAuthorizationService
- [Command Attributes](commands/command-attributes.md) - [IgnoreCommand], [CommandName], etc.
**Quick example:**
```csharp
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/README.md)
Queries represent read operations:
- [Queries Overview](queries/README.md) - Introduction to queries
- [Basic Queries](queries/basic-queries.md) - Simple query handlers
- [Query Registration](queries/query-registration.md) - Registration patterns
- [Query Authorization](queries/query-authorization.md) - IQueryAuthorizationService
- [Query Attributes](queries/query-attributes.md) - [IgnoreQuery], [QueryName], etc.
**Quick example:**
```csharp
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](validation/README.md)
Input validation with FluentValidation:
- [Validation Overview](validation/README.md) - Introduction to validation
- [FluentValidation Setup](validation/fluentvalidation-setup.md) - Setting up validators
- [HTTP Validation](validation/http-validation.md) - RFC 7807 Problem Details
- [gRPC Validation](validation/grpc-validation.md) - Google Rich Error Model
- [Custom Validation](validation/custom-validation.md) - Custom validation scenarios
**Quick example:**
```csharp
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](dynamic-queries/README.md)
OData-like querying capabilities:
- [Dynamic Queries Overview](dynamic-queries/README.md) - Introduction to dynamic queries
- [Getting Started](dynamic-queries/getting-started.md) - First dynamic query
- [Filters and Sorts](dynamic-queries/filters-and-sorts.md) - Filtering, sorting, paging
- [Groups and Aggregates](dynamic-queries/groups-and-aggregates.md) - Grouping and aggregation
- [Queryable Providers](dynamic-queries/queryable-providers.md) - IQueryableProvider implementation
- [Alter Queryable Services](dynamic-queries/alter-queryable-services.md) - Security filters, tenant isolation
- [Interceptors](dynamic-queries/interceptors.md) - IDynamicQueryInterceptorProvider
**Quick example:**
```csharp
// 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
```csharp
// 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
```csharp
// Query always returns result
public interface IQueryHandler<in TQuery, TResult>
{
Task<TResult> HandleAsync(TQuery query, CancellationToken cancellationToken = default);
}
```
### Dynamic Query Interfaces
```csharp
// 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
```csharp
// 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
```csharp
// 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
```csharp
// 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
```csharp
// 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](commands/README.md)** - Master write operations
2. **[Queries](queries/README.md)** - Master read operations
3. **[Validation](validation/README.md)** - Add input validation
4. **[Dynamic Queries](dynamic-queries/README.md)** - Advanced querying
## See Also
- [Getting Started](../getting-started/README.md) - Build your first application
- [Architecture](../architecture/README.md) - Understanding the framework design
- [Best Practices](../best-practices/README.md) - Production-ready patterns
- [Tutorials](../tutorials/README.md) - Comprehensive examples