9.3 KiB
Architecture
Understand the design principles, patterns, and extensibility of Svrnty.CQRS.
Overview
Svrnty.CQRS is built on solid architectural principles that make it:
- ✅ Metadata-driven - Runtime discovery through compile-time registration
- ✅ Modular - Clear separation between abstractions and implementations
- ✅ Extensible - Multiple extension points for customization
- ✅ Convention-based - Minimal configuration with sensible defaults
- ✅ Type-safe - Compile-time type checking with runtime flexibility
Architecture Topics
CQRS Pattern
Deep dive into the Command Query Responsibility Segregation pattern:
- Separation of reads and writes
- Benefits and trade-offs
- When to use CQRS
- Common anti-patterns
- Implementation patterns
Metadata Discovery
How Svrnty.CQRS uses metadata for automatic endpoint generation:
- Metadata registration pattern
- Discovery services (ICommandDiscovery, IQueryDiscovery)
- Runtime enumeration
- Endpoint generation process
- Type safety with generics
Modular Solution Structure
Best practices for organizing your solution into layers:
- Multi-project solution structure
- Api → CQRS → Domain → DAL dependencies
- Separation of concerns
- Project references
- Real-world example
Dependency Injection
DI patterns and handler registration:
- Service registration patterns
- Handler lifetime management
- Scoped vs Transient vs Singleton
- Constructor injection
- Service resolution
Extensibility Points
Framework extension mechanisms:
- Custom authorization services
- Query alteration services
- Dynamic query interceptors
- Custom attributes
- Middleware integration
Key Architectural Concepts
1. Abstractions vs Implementations
Svrnty.CQRS separates interfaces from implementations:
Svrnty.CQRS.Abstractions (interfaces only)
↓ depends on
Svrnty.CQRS (core implementation)
↓ depends on
Svrnty.CQRS.MinimalApi (HTTP integration)
Svrnty.CQRS.Grpc (gRPC integration)
Benefits:
- Consumer projects reference only abstractions
- Minimal dependencies
- Easy to swap implementations
- Clear contracts
2. Metadata-Driven Discovery
Instead of scanning assemblies at runtime, Svrnty.CQRS uses explicit metadata:
// Registration creates metadata
services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();
// Metadata stored as singleton
services.AddSingleton<ICommandMeta>(new CommandMeta<CreateUserCommand, int, CreateUserCommandHandler>());
// Discovery queries metadata
public class CommandDiscovery : ICommandDiscovery
{
private readonly IEnumerable<ICommandMeta> _metas;
public IEnumerable<ICommandMeta> GetCommands() => _metas;
}
Benefits:
- No reflection-heavy assembly scanning
- Faster startup
- AOT-compatible
- Explicit control
3. Convention Over Configuration
Minimal configuration with smart defaults:
// Default naming convention
CreateUserCommand → POST /api/command/createUser
// Custom naming
[CommandName("register")]
CreateUserCommand → POST /api/command/register
// Default route prefix
/api/command/* and /api/query/*
4. Type Safety
Compile-time type safety with generic constraints:
// Type-safe registration
services.AddCommand<TCommand, TResult, THandler>()
where THandler : ICommandHandler<TCommand, TResult>;
// Compile error if types don't match
services.AddCommand<CreateUserCommand, int, WrongHandler>(); // ❌ Compile error
Architectural Layers
Typical Application Structure
┌─────────────────────────────────────────┐
│ Presentation Layer │
│ (HTTP Endpoints, gRPC Services) │
│ - Svrnty.CQRS.MinimalApi │
│ - Svrnty.CQRS.Grpc │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ Application Layer │
│ (Commands, Queries, Handlers) │
│ - Command/Query definitions │
│ - Handler implementations │
│ - Validators │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ Domain Layer │
│ (Business logic, Entities, Events) │
│ - Domain models │
│ - Business rules │
│ - Domain events │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ Infrastructure Layer │
│ (Data access, External services) │
│ - Repositories │
│ - Database context │
│ - External API clients │
└─────────────────────────────────────────┘
Multi-Project Solution
For larger applications, use multiple projects:
MySolution/
├── MySolution.Api/ # HTTP/gRPC endpoints
├── MySolution.CQRS/ # Commands, queries, handlers
├── MySolution.Domain/ # Domain models, events
├── MySolution.Infrastructure/ # EF Core, repositories
└── MySolution.Tests/ # Unit and integration tests
Design Patterns Used
1. Command Pattern
Commands encapsulate requests as objects:
// Command (request object)
public record CreateUserCommand
{
public string Name { get; init; } = string.Empty;
}
// Handler (executes the request)
public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, int>
{
public Task<int> HandleAsync(CreateUserCommand command, CancellationToken cancellationToken)
{
// Execute logic
}
}
2. Mediator Pattern
CQRS acts as a mediator between API and business logic:
Client → Endpoint → Handler → Business Logic
No direct dependencies between client and business logic.
3. Strategy Pattern
Multiple implementations of same interface:
ICommandHandler<CreateUserCommand, int>
→ CreateUserCommandHandler
→ CreateUserWithEmailCommandHandler
→ CreateUserWithSSOCommandHandler
4. Decorator Pattern
Validators, authorization, logging wrap handlers:
Client → Validation → Authorization → Handler → Business Logic
Extensibility Architecture
Extension Points
-
Authorization
ICommandAuthorizationService<TCommand>IQueryAuthorizationService<TQuery>
-
Query Alteration
IAlterQueryableService<TSource, TDestination>
-
Dynamic Query Interceptors
IDynamicQueryInterceptorProvider
-
Attributes
[CommandName],[QueryName][IgnoreCommand],[IgnoreQuery]
-
Middleware
- ASP.NET Core pipeline integration
- Custom filters
Performance Considerations
Startup Performance
- Fast startup - Metadata pattern avoids assembly scanning
- Minimal reflection - Type information captured at registration
- AOT-friendly - No runtime type discovery
Runtime Performance
- Direct handler invocation - No mediator overhead
- DI container resolution - Standard ASP.NET Core performance
- Endpoint routing - Uses built-in routing (HTTP) or gRPC runtime
Memory Efficiency
- Singleton metadata - One instance per command/query type
- Scoped handlers - Created per request, disposed after
- No caching layer - Direct execution
Security Architecture
Defense in Depth
1. Network Layer (HTTPS, firewall)
2. Authentication (JWT, API keys)
3. Authorization (IAuthorizationService)
4. Validation (FluentValidation)
5. Business Rules (in handlers)
6. Data Access (parameterized queries)
Built-in Security Features
- ✅ Validation before execution (FluentValidation)
- ✅ Authorization services (per command/query)
- ✅ Attribute-based endpoint control ([Ignore])
- ✅ Integration with ASP.NET Core auth
What's Next?
Explore specific architectural topics:
- CQRS Pattern - Deep dive into CQRS
- Metadata Discovery - How discovery works
- Modular Solution Structure - Best practices for organization
- Dependency Injection - DI patterns
- Extensibility Points - Customization mechanisms
See Also
- Getting Started - Build your first application
- Best Practices - Production-ready patterns
- Tutorials: Modular Solution - Step-by-step guide