415 lines
12 KiB
Markdown
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
|