1027 lines
40 KiB
Markdown
1027 lines
40 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Project Overview
|
|
|
|
This is Svrnty.CQRS, a modern implementation of Command Query Responsibility Segregation (CQRS) for .NET 10. It was forked from PoweredSoft.CQRS and provides:
|
|
|
|
- CQRS pattern implementation with command/query handlers exposed via HTTP or gRPC
|
|
- Automatic HTTP endpoint generation via Minimal API
|
|
- Automatic gRPC endpoint generation with source generators and Google Rich Error Model validation
|
|
- Dynamic query capabilities (filtering, sorting, grouping, aggregation)
|
|
- FluentValidation support with RFC 7807 Problem Details (HTTP) and Google Rich Error Model (gRPC)
|
|
- AOT (Ahead-of-Time) compilation compatibility for core packages (where dependencies allow)
|
|
|
|
## Solution Structure
|
|
|
|
The solution contains 17 projects organized by responsibility (16 packages + 1 sample project):
|
|
|
|
**Abstractions (interfaces and contracts only):**
|
|
- `Svrnty.CQRS.Abstractions` - Core interfaces (ICommandHandler, IQueryHandler, discovery contracts)
|
|
- `Svrnty.CQRS.DynamicQuery.Abstractions` - Dynamic query interfaces (multi-targets netstandard2.1 and net10.0)
|
|
- `Svrnty.CQRS.Grpc.Abstractions` - gRPC-specific interfaces and contracts
|
|
- `Svrnty.CQRS.Events.Abstractions` - Event streaming interfaces and models
|
|
- `Svrnty.CQRS.Events.ConsumerGroups.Abstractions` - Consumer group coordination interfaces
|
|
|
|
**Implementation:**
|
|
- `Svrnty.CQRS` - Core discovery and registration logic
|
|
- `Svrnty.CQRS.MinimalApi` - Minimal API endpoint mapping for commands/queries (recommended for HTTP)
|
|
- `Svrnty.CQRS.DynamicQuery` - PoweredSoft.DynamicQuery integration for advanced filtering
|
|
- `Svrnty.CQRS.DynamicQuery.MinimalApi` - Minimal API endpoint mapping for dynamic queries
|
|
- `Svrnty.CQRS.FluentValidation` - Validation integration helpers
|
|
- `Svrnty.CQRS.Grpc` - gRPC service implementation support
|
|
- `Svrnty.CQRS.Grpc.Generators` - Source generator for .proto files and gRPC service implementations
|
|
- `Svrnty.CQRS.Events` - Core event streaming implementation
|
|
- `Svrnty.CQRS.Events.Grpc` - gRPC bidirectional streaming for events
|
|
- `Svrnty.CQRS.Events.PostgreSQL` - PostgreSQL storage for persistent and ephemeral streams
|
|
- `Svrnty.CQRS.Events.ConsumerGroups` - Consumer group coordination with PostgreSQL backend
|
|
|
|
**Sample Projects:**
|
|
- `Svrnty.Sample` - Comprehensive demo project showcasing both HTTP and gRPC endpoints
|
|
|
|
**Key Design Principle:** Abstractions projects contain ONLY interfaces/attributes with minimal dependencies. Implementation projects depend on abstractions. This allows consumers to reference abstractions without pulling in heavy implementation dependencies.
|
|
|
|
## Build Commands
|
|
|
|
```bash
|
|
# Restore dependencies
|
|
dotnet restore
|
|
|
|
# Build entire solution
|
|
dotnet build
|
|
|
|
# Build in Release mode
|
|
dotnet build -c Release
|
|
|
|
# Create NuGet packages (with version)
|
|
dotnet pack -c Release -o ./artifacts -p:Version=1.0.0
|
|
|
|
# Build specific project
|
|
dotnet build Svrnty.CQRS/Svrnty.CQRS.csproj
|
|
```
|
|
|
|
## Testing
|
|
|
|
This repository does not currently contain test projects. When adding tests:
|
|
- Place them in a `tests/` directory or alongside source projects
|
|
- Name them with `.Tests` suffix (e.g., `Svrnty.CQRS.Tests`)
|
|
|
|
## Architecture
|
|
|
|
### Core CQRS Pattern
|
|
|
|
The framework uses handler interfaces that follow this pattern:
|
|
|
|
```csharp
|
|
// Command with no result
|
|
ICommandHandler<TCommand>
|
|
Task HandleAsync(TCommand command, CancellationToken cancellationToken = default)
|
|
|
|
// Command with result
|
|
ICommandHandler<TCommand, TResult>
|
|
Task<TResult> HandleAsync(TCommand command, CancellationToken cancellationToken = default)
|
|
|
|
// Query (always returns result)
|
|
IQueryHandler<TQuery, TResult>
|
|
Task<TResult> HandleAsync(TQuery query, CancellationToken cancellationToken = default)
|
|
```
|
|
|
|
### Metadata-Driven Discovery
|
|
|
|
The framework uses a **metadata pattern** for runtime discovery:
|
|
|
|
1. When you register a handler using `services.AddCommand<TCommand, THandler>()`, it:
|
|
- Registers the handler in DI as `ICommandHandler<TCommand, THandler>`
|
|
- Creates metadata (`ICommandMeta`) describing the command type, handler type, and result type
|
|
- Stores metadata as singleton in DI
|
|
|
|
2. Discovery services (`ICommandDiscovery`, `IQueryDiscovery`) implemented in `Svrnty.CQRS`:
|
|
- Query all registered metadata from DI container
|
|
- Provide lookup methods: `GetCommand(string name)`, `GetCommands()`, etc.
|
|
|
|
3. Endpoint mapping (HTTP and gRPC) uses discovery to:
|
|
- Enumerate all registered commands/queries
|
|
- Dynamically generate endpoints at application startup
|
|
- Apply naming conventions (convert to lowerCamelCase)
|
|
- Generate gRPC service implementations via source generators
|
|
|
|
**Key Files:**
|
|
- `Svrnty.CQRS.Abstractions/Discovery/` - Metadata interfaces
|
|
- `Svrnty.CQRS/Discovery/` - Discovery implementations
|
|
- `Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs` - HTTP endpoint generation
|
|
- `Svrnty.CQRS.DynamicQuery.MinimalApi/EndpointRouteBuilderExtensions.cs` - Dynamic query endpoint generation
|
|
- `Svrnty.CQRS.Grpc.Generators/` - gRPC service generation via source generators
|
|
|
|
### Integration Options
|
|
|
|
There are two primary integration options for exposing commands and queries:
|
|
|
|
#### Option 1: gRPC (Recommended for performance-critical scenarios)
|
|
|
|
The **Svrnty.CQRS.Grpc** package with **Svrnty.CQRS.Grpc.Generators** source generator provides high-performance gRPC endpoints:
|
|
|
|
**Registration:**
|
|
```csharp
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// Register CQRS services
|
|
builder.Services.AddSvrntyCQRS();
|
|
builder.Services.AddDefaultCommandDiscovery();
|
|
builder.Services.AddDefaultQueryDiscovery();
|
|
|
|
// Add your commands and queries
|
|
builder.Services.AddCommand<AddUserCommand, int, AddUserCommandHandler>();
|
|
builder.Services.AddCommand<RemoveUserCommand, RemoveUserCommandHandler>();
|
|
|
|
// Add gRPC support
|
|
builder.Services.AddGrpc();
|
|
|
|
var app = builder.Build();
|
|
|
|
// Map auto-generated gRPC service implementations
|
|
app.MapGrpcService<CommandServiceImpl>();
|
|
app.MapGrpcService<QueryServiceImpl>();
|
|
|
|
// Enable gRPC reflection for tools like grpcurl
|
|
app.MapGrpcReflectionService();
|
|
|
|
app.Run();
|
|
```
|
|
|
|
**How It Works:**
|
|
1. Define `.proto` files in `Protos/` directory with your commands/queries as messages
|
|
2. Source generator automatically creates `CommandServiceImpl` and `QueryServiceImpl` implementations
|
|
3. Property names in C# commands must match proto field names (case-insensitive)
|
|
4. FluentValidation is automatically integrated with **Google Rich Error Model** for structured validation errors
|
|
5. Validation errors return `google.rpc.Status` with `BadRequest` containing `FieldViolations`
|
|
|
|
**Features:**
|
|
- High-performance binary protocol
|
|
- Automatic service implementation generation at compile time
|
|
- Google Rich Error Model for structured validation errors
|
|
- Full FluentValidation integration
|
|
- gRPC reflection support for development tools
|
|
- Suitable for microservices, internal APIs, and low-latency scenarios
|
|
|
|
**Key Files:**
|
|
- `Svrnty.CQRS.Grpc/` - Runtime support for gRPC services
|
|
- `Svrnty.CQRS.Grpc.Generators/` - Source generator for service implementations
|
|
|
|
#### Option 2: HTTP via Minimal API (Recommended for web/browser scenarios)
|
|
|
|
The **Svrnty.CQRS.MinimalApi** package provides HTTP endpoints for CQRS commands and queries:
|
|
|
|
**Registration:**
|
|
```csharp
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// Register CQRS services
|
|
builder.Services.AddSvrntyCQRS();
|
|
builder.Services.AddDefaultCommandDiscovery();
|
|
builder.Services.AddDefaultQueryDiscovery();
|
|
|
|
// Add your commands and queries
|
|
builder.Services.AddCommand<CreatePersonCommand, CreatePersonCommandHandler>();
|
|
builder.Services.AddQuery<PersonQuery, IQueryable<Person>, PersonQueryHandler>();
|
|
|
|
// Add Swagger (optional)
|
|
builder.Services.AddEndpointsApiExplorer();
|
|
builder.Services.AddSwaggerGen();
|
|
|
|
var app = builder.Build();
|
|
|
|
// Map endpoints (this creates routes automatically)
|
|
app.MapSvrntyCommands(); // Maps all commands to POST /api/command/{name}
|
|
app.MapSvrntyQueries(); // Maps all queries to POST/GET /api/query/{name}
|
|
|
|
app.Run();
|
|
```
|
|
|
|
**How It Works:**
|
|
1. Extension methods iterate through `ICommandDiscovery` and `IQueryDiscovery`
|
|
2. For each command/query, creates Minimal API endpoints using `MapPost()`/`MapGet()`
|
|
3. Applies naming conventions (lowerCamelCase)
|
|
4. Respects `[CommandControllerIgnore]` and `[QueryControllerIgnore]` attributes
|
|
5. Integrates with `ICommandAuthorizationService` and `IQueryAuthorizationService`
|
|
6. Supports OpenAPI/Swagger documentation
|
|
|
|
**Features:**
|
|
- Queries support both POST (with JSON body) and GET (with query string parameters)
|
|
- Commands only support POST with JSON body
|
|
- Authorization via authorization services (returns 401/403 status codes)
|
|
- Customizable route prefixes: `MapSvrntyCommands("my-prefix")`
|
|
- Automatic OpenAPI tags: "Commands" and "Queries"
|
|
- RFC 7807 Problem Details for validation errors
|
|
- Full Swagger/OpenAPI support
|
|
|
|
**Key Files:**
|
|
- `Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs` - Main implementation
|
|
|
|
#### Option 3: Both gRPC and HTTP (Dual Protocol Support)
|
|
|
|
You can enable both protocols simultaneously, allowing clients to choose their preferred protocol:
|
|
|
|
```csharp
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// Register CQRS services
|
|
builder.Services.AddSvrntyCQRS();
|
|
builder.Services.AddDefaultCommandDiscovery();
|
|
builder.Services.AddDefaultQueryDiscovery();
|
|
|
|
// Add commands and queries
|
|
AddCommands(builder.Services);
|
|
AddQueries(builder.Services);
|
|
|
|
// Add both gRPC and HTTP support
|
|
builder.Services.AddGrpc();
|
|
builder.Services.AddEndpointsApiExplorer();
|
|
builder.Services.AddSwaggerGen();
|
|
|
|
var app = builder.Build();
|
|
|
|
// Map both gRPC and HTTP endpoints
|
|
app.MapGrpcService<CommandServiceImpl>();
|
|
app.MapGrpcService<QueryServiceImpl>();
|
|
app.MapGrpcReflectionService();
|
|
|
|
app.MapSvrntyCommands();
|
|
app.MapSvrntyQueries();
|
|
|
|
app.Run();
|
|
```
|
|
|
|
**Benefits:**
|
|
- Single codebase supports multiple protocols
|
|
- gRPC for high-performance, low-latency scenarios (microservices, internal APIs)
|
|
- HTTP for web browsers, legacy clients, and public APIs
|
|
- Same commands, queries, and validation logic for both protocols
|
|
- Swagger UI available for HTTP endpoints, gRPC reflection for gRPC clients
|
|
|
|
### Dynamic Query System
|
|
|
|
Dynamic queries provide OData-like filtering capabilities:
|
|
|
|
**Core Components:**
|
|
- `IDynamicQuery<TSource, TDestination>` - Interface with GetFilters(), GetSorts(), GetGroups(), GetAggregates()
|
|
- `IQueryableProvider<TSource>` - Provides base IQueryable to query against
|
|
- `IAlterQueryableService<TSource, TDestination>` - Middleware to modify queries (e.g., security filters)
|
|
- `DynamicQueryHandler<TSource, TDestination>` - Executes queries using PoweredSoft.DynamicQuery
|
|
|
|
**Request Flow:**
|
|
1. HTTP request with filters/sorts/aggregates
|
|
2. Minimal API endpoint receives request
|
|
3. DynamicQueryHandler gets base queryable from IQueryableProvider
|
|
4. Applies alterations from all registered IAlterQueryableService instances
|
|
5. Builds PoweredSoft query criteria
|
|
6. Executes and returns IQueryExecutionResult
|
|
|
|
**Registration Example:**
|
|
```csharp
|
|
// Register dynamic query
|
|
services.AddDynamicQuery<Person, PersonDto>()
|
|
.AddDynamicQueryWithProvider<Person, PersonQueryableProvider>()
|
|
.AddAlterQueryable<Person, PersonDto, SecurityFilter>();
|
|
|
|
// Map dynamic query endpoints
|
|
app.MapSvrntyDynamicQueries(); // Creates POST/GET /api/query/{queryName} endpoints
|
|
```
|
|
|
|
**Key Files:**
|
|
- `Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs` - Query execution logic
|
|
- `Svrnty.CQRS.DynamicQuery.MinimalApi/EndpointRouteBuilderExtensions.cs` - HTTP endpoint mapping
|
|
|
|
## Package Configuration
|
|
|
|
All projects target .NET 10.0 and use C# 14, sharing common configuration:
|
|
|
|
- **Target Framework**: `net10.0` (except DynamicQuery.Abstractions which multi-targets `netstandard2.1;net10.0`)
|
|
- **Language Version**: C# 14
|
|
- **IsAotCompatible**: Currently set but not enforced (many dependencies are not AOT-compatible yet)
|
|
- **Symbols**: Portable debug symbols with source, published as `.snupkg`
|
|
- **NuGet metadata**: Icon, README, license (MIT), and repository URL included in packages
|
|
- **Authors**: David Lebee, Mathias Beaulieu-Duncan
|
|
- **Repository**: https://git.openharbor.io/svrnty/dotnet-cqrs
|
|
|
|
### Package Dependencies
|
|
|
|
**Core Dependencies:**
|
|
- **Microsoft.Extensions.DependencyInjection.Abstractions**: 10.0.0
|
|
- **FluentValidation**: 11.11.0
|
|
- **PoweredSoft.DynamicQuery**: 3.0.1
|
|
- **Pluralize.NET**: 1.0.2
|
|
|
|
**gRPC Dependencies (for Svrnty.CQRS.Grpc):**
|
|
- **Grpc.AspNetCore**: 2.68.0 or later
|
|
- **Grpc.AspNetCore.Server.Reflection**: 2.71.0 or later (optional, for reflection)
|
|
- **Grpc.StatusProto**: 2.71.0 or later (for Rich Error Model validation)
|
|
- **Grpc.Tools**: 2.76.0 or later (for .proto compilation)
|
|
|
|
**Source Generator Dependencies (for Svrnty.CQRS.Grpc.Generators):**
|
|
- **Microsoft.CodeAnalysis.CSharp**: 5.0.0-2.final
|
|
- **Microsoft.CodeAnalysis.Analyzers**: 3.11.0
|
|
- **Microsoft.Build.Utilities.Core**: 17.0.0
|
|
- Targets: netstandard2.0 (for Roslyn compatibility)
|
|
|
|
## Publishing
|
|
|
|
NuGet packages are published automatically via GitHub Actions when a release is created:
|
|
|
|
**Workflow:** `.github/workflows/publish-nugets.yml`
|
|
1. Triggered on release publication
|
|
2. Extracts version from release tag
|
|
3. Runs `dotnet pack -c Release -p:Version={tag}`
|
|
4. Pushes to NuGet.org using `NUGET_API_KEY` secret
|
|
|
|
**Manual publish:**
|
|
```bash
|
|
# Create packages with specific version
|
|
dotnet pack -c Release -o ./artifacts -p:Version=1.2.3
|
|
|
|
# Push to NuGet
|
|
dotnet nuget push ./artifacts/*.nupkg --source https://api.nuget.org/v3/index.json --api-key YOUR_KEY
|
|
```
|
|
|
|
## Development Workflow
|
|
|
|
**Adding a New Command/Query Handler:**
|
|
|
|
1. Create command/query POCO in consumer project
|
|
2. Implement handler: `ICommandHandler<TCommand, TResult>`
|
|
3. Register in DI: `services.AddCommand<CreatePersonCommand, CreatePersonCommandHandler>()`
|
|
4. (Optional) Add validator: `services.AddTransient<IValidator<CreatePersonCommand>, Validator>()`
|
|
5. Controller endpoint is automatically generated
|
|
|
|
**Adding a New Feature to Framework:**
|
|
|
|
1. Add interface to appropriate Abstractions project
|
|
2. Implement in corresponding implementation project
|
|
3. Update ServiceCollectionExtensions with registration method
|
|
4. Ensure all projects maintain AOT compatibility (unless AspNetCore-specific)
|
|
5. Update package version and release notes
|
|
|
|
**Naming Conventions:**
|
|
|
|
- Commands/Queries: Use `[CommandName]` or `[QueryName]` attribute for custom names
|
|
- Default naming: Strips "Command"/"Query" suffix, converts to lowerCamelCase
|
|
- Example: `CreatePersonCommand` -> `createPerson` endpoint
|
|
|
|
## C# 14 Language Features
|
|
|
|
The project now uses C# 14, which introduces several new features. Be aware of these breaking changes:
|
|
|
|
**Potential Breaking Changes:**
|
|
- **`field` keyword**: New contextual keyword in property accessors for implicit backing fields
|
|
- **`extension` keyword**: Reserved for extension containers; use `@extension` for identifiers
|
|
- **`partial` return type**: Cannot use `partial` as return type without escaping
|
|
- **Span<T> overload resolution**: New implicit conversions may select different overloads
|
|
- **`scoped` as lambda modifier**: Always treated as modifier in lambda parameters
|
|
|
|
**New Features Available:**
|
|
- Extension members (static extension members and extension properties)
|
|
- Implicit span conversions
|
|
- Unbound generic types with `nameof`
|
|
- Lambda parameter modifiers without type specification
|
|
- Partial instance constructors and events
|
|
- Null-conditional assignment (`?.=` and `?[]=`)
|
|
|
|
The codebase currently compiles without warnings on C# 14.
|
|
|
|
## Important Implementation Notes
|
|
|
|
1. **AOT Compatibility**: Currently not enforced. The `IsAotCompatible` property is set on some projects but many dependencies (including FluentValidation, PoweredSoft.DynamicQuery) are not AOT-compatible. Future work may address this.
|
|
|
|
2. **Async Everywhere**: All handlers are async. Always support CancellationToken.
|
|
|
|
3. **Generic Type Safety**: Framework relies heavily on generics for compile-time safety. When adding features, maintain strong typing.
|
|
|
|
4. **Metadata Pattern**: When extending discovery, always create corresponding metadata classes (implement ICommandMeta/IQueryMeta).
|
|
|
|
5. **Endpoint Mapping Timing**: Endpoints are mapped at application startup. Discovery services must be registered before calling `MapSvrntyCommands()`/`MapSvrntyQueries()` or mapping gRPC services.
|
|
|
|
6. **FluentValidation Integration**:
|
|
- For HTTP: Validation happens automatically in the Minimal API pipeline. Errors return RFC 7807 Problem Details.
|
|
- For gRPC: Validation happens automatically via source-generated services. Errors return Google Rich Error Model with structured FieldViolations.
|
|
- The framework REGISTERS validators in DI; actual validation execution is handled by the endpoint implementations.
|
|
|
|
7. **DynamicQuery Interceptors**: Support up to 5 interceptors per query type. Interceptors modify PoweredSoft DynamicQuery behavior.
|
|
|
|
## Event Streaming Architecture
|
|
|
|
The framework provides comprehensive event streaming support with persistent (event sourcing) and ephemeral (message queue) streams.
|
|
|
|
### Core Components
|
|
|
|
**Storage Abstraction** - `IEventStreamStore`:
|
|
- `AppendAsync()` - Add events to persistent streams (append-only log)
|
|
- `ReadStreamAsync()` - Read events from offset (for event replay and consumer groups)
|
|
- `EnqueueAsync()` - Add messages to ephemeral streams (queue)
|
|
- `DequeueAsync()` - Pull messages with visibility timeout (at-least-once delivery)
|
|
- `AcknowledgeAsync()` / `NackAsync()` - Confirm processing or requeue
|
|
|
|
**Consumer Groups** - `IConsumerGroupReader` and `IConsumerOffsetStore`:
|
|
- Coordinate multiple consumers processing the same stream without duplicates
|
|
- Track consumer offsets for fault-tolerant consumption
|
|
- Automatic heartbeat monitoring and stale consumer cleanup
|
|
- Flexible commit strategies (Manual, AfterEach, AfterBatch, Periodic)
|
|
- At-least-once delivery guarantees
|
|
|
|
**gRPC Streaming** - `EventStreamServiceImpl`:
|
|
- Bidirectional streaming for real-time event delivery
|
|
- Subscription modes: Broadcast (all events) or Queue (dequeue with ack)
|
|
- Persistent and ephemeral stream support
|
|
|
|
### Consumer Groups
|
|
|
|
Consumer groups enable load balancing and fault tolerance for stream processing:
|
|
|
|
```csharp
|
|
// Register consumer groups
|
|
builder.Services.AddPostgresConsumerGroups(
|
|
builder.Configuration.GetSection("EventStreaming:ConsumerGroups"));
|
|
|
|
// Consume stream with automatic offset management
|
|
var reader = serviceProvider.GetRequiredService<IConsumerGroupReader>();
|
|
|
|
await foreach (var @event in reader.ConsumeAsync(
|
|
streamName: "orders",
|
|
groupId: "order-processors",
|
|
consumerId: "worker-1",
|
|
options: new ConsumerGroupOptions
|
|
{
|
|
BatchSize = 100,
|
|
CommitStrategy = OffsetCommitStrategy.AfterBatch,
|
|
HeartbeatInterval = TimeSpan.FromSeconds(10),
|
|
SessionTimeout = TimeSpan.FromSeconds(30)
|
|
},
|
|
cancellationToken))
|
|
{
|
|
await ProcessEventAsync(@event);
|
|
// Offset auto-committed after batch
|
|
}
|
|
```
|
|
|
|
**Key Features:**
|
|
- **Automatic Offset Management**: Tracks last processed position per consumer
|
|
- **Heartbeat Monitoring**: Background service detects and removes stale consumers
|
|
- **Commit Strategies**: Manual, AfterEach, AfterBatch, Periodic
|
|
- **Load Balancing**: Multiple consumers coordinate to process stream
|
|
- **Fault Tolerance**: Resume from last committed offset after failure
|
|
- **Consumer Discovery**: Query active consumers and their offsets
|
|
|
|
**Database Schema:**
|
|
- `consumer_offsets` - Stores committed offsets per consumer
|
|
- `consumer_registrations` - Tracks active consumers with heartbeats
|
|
- `cleanup_stale_consumers()` - Function to remove dead consumers
|
|
- `consumer_group_status` - View for monitoring consumer health
|
|
|
|
### Retention Policies
|
|
|
|
Retention policies provide automatic event cleanup based on age or size limits:
|
|
|
|
```csharp
|
|
// Register retention policy service
|
|
builder.Services.AddPostgresRetentionPolicies(options =>
|
|
{
|
|
options.Enabled = true;
|
|
options.CleanupInterval = TimeSpan.FromHours(1);
|
|
options.CleanupWindowStart = TimeSpan.FromHours(2); // 2 AM UTC
|
|
options.CleanupWindowEnd = TimeSpan.FromHours(6); // 6 AM UTC
|
|
options.UseCleanupWindow = true;
|
|
});
|
|
|
|
// Set retention policies
|
|
var policyStore = serviceProvider.GetRequiredService<IRetentionPolicyStore>();
|
|
|
|
// Time-based retention
|
|
await policyStore.SetPolicyAsync(new RetentionPolicyConfig
|
|
{
|
|
StreamName = "orders",
|
|
MaxAge = TimeSpan.FromDays(30),
|
|
Enabled = true
|
|
});
|
|
|
|
// Size-based retention
|
|
await policyStore.SetPolicyAsync(new RetentionPolicyConfig
|
|
{
|
|
StreamName = "analytics",
|
|
MaxEventCount = 10000,
|
|
Enabled = true
|
|
});
|
|
|
|
// Combined retention
|
|
await policyStore.SetPolicyAsync(new RetentionPolicyConfig
|
|
{
|
|
StreamName = "logs",
|
|
MaxAge = TimeSpan.FromDays(7),
|
|
MaxEventCount = 50000,
|
|
Enabled = true
|
|
});
|
|
|
|
// Default policy for all streams
|
|
await policyStore.SetPolicyAsync(new RetentionPolicyConfig
|
|
{
|
|
StreamName = "*",
|
|
MaxAge = TimeSpan.FromDays(90),
|
|
Enabled = true
|
|
});
|
|
```
|
|
|
|
**Key Features:**
|
|
- **Time-based Retention**: Delete events older than configured age
|
|
- **Size-based Retention**: Keep only last N events per stream
|
|
- **Wildcard Policies**: "*" stream name applies to all streams
|
|
- **Cleanup Windows**: Run cleanup during specific UTC time windows
|
|
- **Background Service**: PeriodicTimer-based scheduled cleanup
|
|
- **Statistics Tracking**: Detailed metrics per cleanup operation
|
|
- **Midnight Crossing**: Window logic handles midnight-spanning windows
|
|
|
|
**Database Schema:**
|
|
- `retention_policies` - Stores policies per stream
|
|
- `apply_time_retention()` - Function for time-based cleanup
|
|
- `apply_size_retention()` - Function for size-based cleanup
|
|
- `apply_all_retention_policies()` - Function to enforce all enabled policies
|
|
- `retention_policy_status` - View for monitoring retention status
|
|
|
|
**Implementation:**
|
|
- `RetentionPolicyService` - BackgroundService enforcing policies
|
|
- `PostgresRetentionPolicyStore` - PostgreSQL implementation of IRetentionPolicyStore
|
|
- `RetentionServiceOptions` - Configuration for cleanup intervals and windows
|
|
- `RetentionCleanupResult` - Statistics about cleanup operations
|
|
|
|
### Event Replay API
|
|
|
|
Event Replay API enables rebuilding projections, reprocessing events, and time-travel debugging:
|
|
|
|
```csharp
|
|
// Register event replay service
|
|
builder.Services.AddPostgresEventReplay();
|
|
|
|
// Replay from offset
|
|
var replayService = serviceProvider.GetRequiredService<IEventReplayService>();
|
|
await foreach (var @event in replayService.ReplayFromOffsetAsync(
|
|
streamName: "orders",
|
|
startOffset: 1000,
|
|
options: new ReplayOptions
|
|
{
|
|
BatchSize = 100,
|
|
MaxEventsPerSecond = 1000,
|
|
EventTypeFilter = new[] { "OrderPlaced", "OrderShipped" },
|
|
ProgressCallback = progress =>
|
|
{
|
|
Console.WriteLine($"{progress.EventsProcessed} events @ {progress.EventsPerSecond:F0} events/sec");
|
|
}
|
|
}))
|
|
{
|
|
await ProcessEventAsync(@event);
|
|
}
|
|
|
|
// Replay from time
|
|
await foreach (var @event in replayService.ReplayFromTimeAsync(
|
|
streamName: "orders",
|
|
startTime: DateTimeOffset.UtcNow.AddDays(-7)))
|
|
{
|
|
await RebuildProjectionAsync(@event);
|
|
}
|
|
|
|
// Replay time range
|
|
await foreach (var @event in replayService.ReplayTimeRangeAsync(
|
|
streamName: "analytics",
|
|
startTime: DateTimeOffset.UtcNow.AddDays(-7),
|
|
endTime: DateTimeOffset.UtcNow.AddDays(-6)))
|
|
{
|
|
await ProcessAnalyticsEventAsync(@event);
|
|
}
|
|
```
|
|
|
|
**Key Features:**
|
|
- **Offset-based Replay**: Replay from specific sequence numbers
|
|
- **Time-based Replay**: Replay from specific timestamps
|
|
- **Time Range Replay**: Replay events within time windows
|
|
- **Event Type Filtering**: Replay only specific event types
|
|
- **Rate Limiting**: Token bucket algorithm for smooth rate control
|
|
- **Progress Tracking**: Callbacks with metrics and estimated completion
|
|
- **Batching**: Efficient streaming with configurable batch sizes
|
|
|
|
**Replay Options:**
|
|
- `BatchSize` - Events to read per database query (default: 100)
|
|
- `MaxEvents` - Maximum events to replay (default: unlimited)
|
|
- `MaxEventsPerSecond` - Rate limit for replay (default: unlimited)
|
|
- `EventTypeFilter` - Filter by event types (default: all)
|
|
- `ProgressCallback` - Monitor progress during replay
|
|
- `ProgressInterval` - How often to invoke callback (default: 1000)
|
|
|
|
**Implementation:**
|
|
- `PostgresEventReplayService` - PostgreSQL implementation of IEventReplayService
|
|
- `ReplayOptions` - Configuration for replay operations
|
|
- `ReplayProgress` - Progress tracking with metrics
|
|
- `RateLimiter` - Internal token bucket rate limiter
|
|
|
|
**Common Use Cases:**
|
|
- Rebuilding read models from scratch
|
|
- Reprocessing events after bug fixes
|
|
- Creating new projections from historical data
|
|
- Time-travel debugging for specific time periods
|
|
- Analytics batch processing with rate limiting
|
|
|
|
### Stream Configuration
|
|
|
|
Stream Configuration provides per-stream configuration for fine-grained control over retention, DLQ, lifecycle, performance, and access control:
|
|
|
|
```csharp
|
|
// Register stream configuration
|
|
builder.Services.AddPostgresStreamConfiguration();
|
|
|
|
// Configure stream with retention
|
|
var configStore = serviceProvider.GetRequiredService<IStreamConfigurationStore>();
|
|
await configStore.SetConfigurationAsync(new StreamConfiguration
|
|
{
|
|
StreamName = "orders",
|
|
Retention = new RetentionConfiguration
|
|
{
|
|
MaxAge = TimeSpan.FromDays(90),
|
|
MaxSizeBytes = 10L * 1024 * 1024 * 1024, // 10 GB
|
|
EnablePartitioning = true
|
|
},
|
|
DeadLetterQueue = new DeadLetterQueueConfiguration
|
|
{
|
|
Enabled = true,
|
|
MaxDeliveryAttempts = 5,
|
|
RetryDelay = TimeSpan.FromMinutes(5)
|
|
},
|
|
Lifecycle = new LifecycleConfiguration
|
|
{
|
|
AutoArchive = true,
|
|
ArchiveAfter = TimeSpan.FromDays(365),
|
|
ArchiveLocation = "s3://archive/orders"
|
|
},
|
|
Performance = new PerformanceConfiguration
|
|
{
|
|
BatchSize = 1000,
|
|
EnableCompression = true,
|
|
EnableIndexing = true,
|
|
IndexedFields = new List<string> { "userId", "tenantId" }
|
|
},
|
|
AccessControl = new AccessControlConfiguration
|
|
{
|
|
AllowedReaders = new List<string> { "admin", "order-service" },
|
|
MaxEventsPerSecond = 10000
|
|
}
|
|
});
|
|
|
|
// Get effective configuration (stream-specific merged with defaults)
|
|
var configProvider = serviceProvider.GetRequiredService<IStreamConfigurationProvider>();
|
|
var effectiveConfig = await configProvider.GetEffectiveConfigurationAsync("orders");
|
|
```
|
|
|
|
**Key Features:**
|
|
- **Per-Stream Configuration**: Override global settings per stream
|
|
- **Retention Policies**: Time, size, and count-based retention per stream
|
|
- **Dead Letter Queues**: Configurable error handling and retry logic
|
|
- **Lifecycle Management**: Automatic archival and deletion
|
|
- **Performance Tuning**: Batch sizes, compression, and indexing
|
|
- **Access Control**: Stream-level permissions and rate limits
|
|
- **Tag-Based Filtering**: Categorize and query streams by tags
|
|
|
|
**Configuration Options:**
|
|
- **Retention**: MaxAge, MaxSizeBytes, MaxEventCount, EnablePartitioning, PartitionInterval
|
|
- **DLQ**: Enabled, DeadLetterStreamName, MaxDeliveryAttempts, RetryDelay
|
|
- **Lifecycle**: AutoCreate, AutoArchive, ArchiveAfter, AutoDelete, DeleteAfter
|
|
- **Performance**: BatchSize, EnableCompression, EnableIndexing, CacheSize
|
|
- **Access Control**: PublicRead/Write, AllowedReaders/Writers, MaxConsumerGroups, MaxEventsPerSecond
|
|
|
|
**Implementation:**
|
|
- `PostgresStreamConfigurationStore` - PostgreSQL implementation of IStreamConfigurationStore
|
|
- `PostgresStreamConfigurationProvider` - Merges stream-specific and global settings
|
|
- `StreamConfiguration` - Main configuration model
|
|
- `RetentionConfiguration`, `DeadLetterQueueConfiguration`, `LifecycleConfiguration`, `PerformanceConfiguration`, `AccessControlConfiguration` - Sub-configuration models
|
|
|
|
**Common Use Cases:**
|
|
- Multi-tenant configuration with different retention per tenant
|
|
- Environment-specific settings (production vs development)
|
|
- Domain-specific configuration (audit logs vs analytics)
|
|
- High-throughput streams with compression and batching
|
|
- Sensitive data streams with access control
|
|
|
|
### Storage Implementations
|
|
|
|
**PostgreSQL** (`Svrnty.CQRS.Events.PostgreSQL`):
|
|
- Persistent streams with offset-based reading
|
|
- Ephemeral streams with SKIP LOCKED for concurrent dequeue
|
|
- Dead letter queue for failed messages
|
|
- Consumer offset tracking and group coordination
|
|
- Retention policy enforcement with automatic cleanup
|
|
- Event replay with rate limiting and progress tracking
|
|
- Per-stream configuration for retention, DLQ, lifecycle, performance, and access control
|
|
- Auto-migration support
|
|
|
|
**In-Memory** (`Svrnty.CQRS.Events`):
|
|
- Fast in-memory storage for development/testing
|
|
- No persistence, data lost on restart
|
|
|
|
### Management, Monitoring & Observability
|
|
|
|
Event streaming includes comprehensive production-ready management, monitoring, and observability features for operational excellence.
|
|
|
|
#### Health Checks
|
|
|
|
Stream and subscription health checks detect consumer lag, stalled consumers, and unhealthy streams:
|
|
|
|
```csharp
|
|
// Register health checks
|
|
builder.Services.AddStreamHealthChecks(options =>
|
|
{
|
|
options.DegradedConsumerLagThreshold = 1000; // Warning at 1000 events lag
|
|
options.UnhealthyConsumerLagThreshold = 10000; // Error at 10000 events lag
|
|
options.DegradedStalledThreshold = TimeSpan.FromMinutes(5); // Warning after 5 min no progress
|
|
options.UnhealthyStalledThreshold = TimeSpan.FromMinutes(15); // Error after 15 min no progress
|
|
});
|
|
|
|
// Use with ASP.NET Core health checks
|
|
builder.Services.AddHealthChecks()
|
|
.AddCheck<StreamHealthCheck>("event-streams");
|
|
|
|
app.MapHealthChecks("/health");
|
|
|
|
// Or use directly
|
|
var healthCheck = serviceProvider.GetRequiredService<IStreamHealthCheck>();
|
|
|
|
// Check specific stream
|
|
var result = await healthCheck.CheckStreamHealthAsync("orders");
|
|
if (result.Status == HealthStatus.Unhealthy)
|
|
{
|
|
Console.WriteLine($"Stream unhealthy: {result.Description}");
|
|
}
|
|
|
|
// Check specific subscription
|
|
var subResult = await healthCheck.CheckSubscriptionHealthAsync("orders", "email-notifications");
|
|
|
|
// Check all streams
|
|
var allStreams = await healthCheck.CheckAllStreamsAsync();
|
|
foreach (var (streamName, health) in allStreams)
|
|
{
|
|
Console.WriteLine($"{streamName}: {health.Status}");
|
|
}
|
|
```
|
|
|
|
**Key Features:**
|
|
- **Lag Detection**: Monitors consumer offset delta from stream head
|
|
- **Stall Detection**: Identifies consumers with no progress over time
|
|
- **Configurable Thresholds**: Separate thresholds for degraded vs unhealthy
|
|
- **ASP.NET Core Integration**: Works with built-in health check system
|
|
- **Bulk Operations**: Check all streams/subscriptions at once
|
|
|
|
**Health States:**
|
|
- `Healthy` - Consumer is keeping up, no lag or delays
|
|
- `Degraded` - Consumer has some lag but within acceptable limits
|
|
- `Unhealthy` - Consumer is severely lagging or stalled
|
|
|
|
#### Metrics & Telemetry
|
|
|
|
OpenTelemetry-compatible metrics using System.Diagnostics.Metrics:
|
|
|
|
```csharp
|
|
// Register metrics
|
|
builder.Services.AddEventStreamMetrics();
|
|
|
|
// Metrics are automatically collected:
|
|
// - svrnty.cqrs.events.published - Counter of published events
|
|
// - svrnty.cqrs.events.consumed - Counter of consumed events
|
|
// - svrnty.cqrs.events.processing_latency - Histogram of processing time
|
|
// - svrnty.cqrs.events.consumer_lag - Gauge of consumer lag
|
|
// - svrnty.cqrs.events.errors - Counter of error events
|
|
// - svrnty.cqrs.events.retries - Counter of retry attempts
|
|
// - svrnty.cqrs.events.stream_length - Gauge of stream size
|
|
// - svrnty.cqrs.events.active_consumers - Gauge of active consumers
|
|
|
|
// Integrate with OpenTelemetry
|
|
builder.Services.AddOpenTelemetry()
|
|
.WithMetrics(metrics => metrics
|
|
.AddMeter("Svrnty.CQRS.Events")
|
|
.AddPrometheusExporter());
|
|
|
|
app.MapPrometheusScrapingEndpoint(); // Expose at /metrics
|
|
|
|
// Use metrics in your code
|
|
var metrics = serviceProvider.GetRequiredService<IEventStreamMetrics>();
|
|
|
|
// Record event published
|
|
metrics.RecordEventPublished("orders", "OrderPlaced");
|
|
|
|
// Record event consumed
|
|
metrics.RecordEventConsumed("orders", "email-notifications", "OrderPlaced");
|
|
|
|
// Record processing latency
|
|
var stopwatch = Stopwatch.StartNew();
|
|
await ProcessEventAsync(evt);
|
|
metrics.RecordProcessingLatency("orders", "email-notifications", stopwatch.Elapsed);
|
|
|
|
// Record consumer lag
|
|
metrics.RecordConsumerLag("orders", "slow-consumer", lag: 5000);
|
|
```
|
|
|
|
**Key Features:**
|
|
- **Zero-allocation Logging**: High-performance metric collection
|
|
- **OpenTelemetry Compatible**: Works with Prometheus, Grafana, Application Insights
|
|
- **Automatic Tags**: All metrics tagged with stream name, subscription ID, event type
|
|
- **Counters**: Events published, consumed, errors, retries
|
|
- **Histograms**: Processing latency distribution
|
|
- **Gauges**: Consumer lag, stream length, active consumers
|
|
|
|
**Grafana Dashboard Examples:**
|
|
```promql
|
|
# Consumer lag by subscription
|
|
svrnty_cqrs_events_consumer_lag{subscription_id="email-notifications"}
|
|
|
|
# Events per second by stream
|
|
rate(svrnty_cqrs_events_published[1m])
|
|
|
|
# P95 processing latency
|
|
histogram_quantile(0.95, svrnty_cqrs_events_processing_latency_bucket)
|
|
|
|
# Error rate
|
|
rate(svrnty_cqrs_events_errors[5m])
|
|
```
|
|
|
|
#### Management API
|
|
|
|
REST API endpoints for operational management:
|
|
|
|
```csharp
|
|
// Register management API
|
|
app.MapEventStreamManagementApi(routePrefix: "api/event-streams");
|
|
|
|
// Available endpoints:
|
|
// GET /api/event-streams - List all streams
|
|
// GET /api/event-streams/{name} - Get stream details
|
|
// GET /api/event-streams/{name}/subscriptions - List subscriptions
|
|
// GET /api/event-streams/subscriptions/{id} - Get subscription details
|
|
// GET /api/event-streams/subscriptions/{id}/consumers/{consumerId} - Get consumer info
|
|
// POST /api/event-streams/subscriptions/{id}/consumers/{consumerId}/reset-offset - Reset offset
|
|
```
|
|
|
|
**Example Usage:**
|
|
|
|
```bash
|
|
# List all streams
|
|
curl http://localhost:5000/api/event-streams
|
|
|
|
# Response:
|
|
[
|
|
{
|
|
"name": "orders",
|
|
"type": "Persistent",
|
|
"deliverySemantics": "AtLeastOnce",
|
|
"scope": "Internal",
|
|
"length": 15234,
|
|
"subscriptionCount": 3,
|
|
"subscriptions": ["email-notifications", "analytics", "inventory-sync"]
|
|
}
|
|
]
|
|
|
|
# Get stream details
|
|
curl http://localhost:5000/api/event-streams/orders
|
|
|
|
# Get subscription details
|
|
curl http://localhost:5000/api/event-streams/subscriptions/email-notifications
|
|
|
|
# Get consumer lag and position
|
|
curl http://localhost:5000/api/event-streams/subscriptions/email-notifications/consumers/worker-1
|
|
|
|
# Response:
|
|
{
|
|
"consumerId": "worker-1",
|
|
"offset": 15000,
|
|
"lag": 234,
|
|
"lastUpdated": "2025-12-10T10:30:00Z",
|
|
"isStalled": false
|
|
}
|
|
|
|
# Reset consumer offset to beginning
|
|
curl -X POST http://localhost:5000/api/event-streams/subscriptions/email-notifications/consumers/worker-1/reset-offset \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"newOffset": 0}'
|
|
|
|
# Reset to latest (skip all lag)
|
|
curl -X POST http://localhost:5000/api/event-streams/subscriptions/email-notifications/consumers/worker-1/reset-offset \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"newOffset": -1}'
|
|
```
|
|
|
|
**Key Features:**
|
|
- **OpenAPI Documentation**: Automatic Swagger documentation
|
|
- **Offset Management**: Reset consumer positions for reprocessing or skip lag
|
|
- **Monitoring Data**: Consumer lag, stream length, subscription status
|
|
- **Operations**: List streams, query subscriptions, manage consumers
|
|
|
|
**Security Considerations:**
|
|
- Add authorization for production: `.RequireAuthorization("AdminOnly")`
|
|
- Consider IP whitelisting for management endpoints
|
|
- Audit log all offset reset operations
|
|
|
|
#### Structured Logging
|
|
|
|
High-performance structured logging using LoggerMessage source generators:
|
|
|
|
```csharp
|
|
using Svrnty.CQRS.Events.Logging;
|
|
|
|
// Correlation context for distributed tracing
|
|
using (CorrelationContext.Begin(correlationId))
|
|
{
|
|
// Stream lifecycle
|
|
_logger.LogStreamCreated("orders", "Persistent", "Internal", "AtLeastOnce");
|
|
_logger.LogSubscriptionRegistered("email-notifications", "orders", "Broadcast");
|
|
_logger.LogConsumerConnected("worker-1", "email-notifications", "orders");
|
|
|
|
// Event publishing
|
|
_logger.LogEventPublished(evt.EventId, evt.GetType().Name, "orders", CorrelationContext.Current);
|
|
|
|
// Event consumption
|
|
var stopwatch = Stopwatch.StartNew();
|
|
await ProcessEventAsync(evt);
|
|
_logger.LogEventConsumed(evt.EventId, evt.GetType().Name, "email-notifications", "worker-1", stopwatch.ElapsedMilliseconds);
|
|
|
|
// Consumer health
|
|
_logger.LogConsumerLagging("slow-consumer", "analytics", lag: 5000);
|
|
_logger.LogConsumerStalled("stalled-consumer", "analytics", TimeSinceUpdate, lag: 10000);
|
|
|
|
// Errors and retries
|
|
_logger.LogEventRetry(evt.EventId, evt.GetType().Name, "order-processing", attemptNumber: 3, maxAttempts: 5);
|
|
_logger.LogEventDeadLettered(evt.EventId, evt.GetType().Name, "order-processing", "Max retries exceeded");
|
|
|
|
// Schema evolution
|
|
_logger.LogEventUpcast(evt.EventId, "UserRegistered", fromVersion: 1, toVersion: 2);
|
|
}
|
|
|
|
// Correlation ID automatically propagates through entire workflow
|
|
```
|
|
|
|
**Key Features:**
|
|
- **Zero-allocation Logging**: LoggerMessage source generators compile logging delegates
|
|
- **Correlation IDs**: AsyncLocal-based propagation across async boundaries
|
|
- **Consistent Event IDs**: Numbered ranges for filtering (1000-1999 streams, 2000-2999 subscriptions, etc.)
|
|
- **Structured Data**: All log parameters are structured for querying
|
|
- **Log Levels**: Appropriate levels (Debug for events, Warning for lag, Error for stalls)
|
|
|
|
**Log Event ID Ranges:**
|
|
- **1000-1999**: Stream lifecycle events
|
|
- **2000-2999**: Subscription lifecycle events
|
|
- **3000-3999**: Consumer lifecycle events
|
|
- **4000-4999**: Event publishing
|
|
- **5000-5999**: Event consumption
|
|
- **6000-6999**: Schema evolution
|
|
- **7000-7999**: Exactly-once delivery
|
|
- **8000-8999**: Cross-service events
|
|
|
|
**Integration Examples:**
|
|
|
|
```csharp
|
|
// Serilog
|
|
Log.Logger = new LoggerConfiguration()
|
|
.MinimumLevel.Debug()
|
|
.Enrich.FromLogContext()
|
|
.WriteTo.Console()
|
|
.WriteTo.Seq("http://localhost:5341")
|
|
.CreateLogger();
|
|
|
|
builder.Host.UseSerilog();
|
|
|
|
// Application Insights
|
|
builder.Services.AddApplicationInsightsTelemetry();
|
|
builder.Logging.AddApplicationInsights();
|
|
|
|
// Query logs by correlation ID
|
|
CorrelationId = "abc-123-def"
|
|
|
|
// Query logs by event type
|
|
EventId >= 4000 AND EventId < 5000 // All publishing events
|
|
|
|
// Query consumer lag warnings
|
|
EventId = 3004 AND Lag > 1000
|
|
```
|
|
|
|
## Common Code Locations
|
|
|
|
- Handler interfaces: `Svrnty.CQRS.Abstractions/ICommandHandler.cs`, `IQueryHandler.cs`
|
|
- Discovery implementations: `Svrnty.CQRS/Discovery/`
|
|
- Service registration: `*/ServiceCollectionExtensions.cs` in each project
|
|
- HTTP endpoint mapping: `Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs`
|
|
- Dynamic query logic: `Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs`
|
|
- Dynamic query endpoints: `Svrnty.CQRS.DynamicQuery.MinimalApi/EndpointRouteBuilderExtensions.cs`
|
|
- gRPC support: `Svrnty.CQRS.Grpc/` runtime, `Svrnty.CQRS.Grpc.Generators/` source generators
|
|
- Event streaming abstractions: `Svrnty.CQRS.Events.Abstractions/IEventStreamStore.cs`, `IEventSubscriptionService.cs`
|
|
- PostgreSQL event storage: `Svrnty.CQRS.Events.PostgreSQL/PostgresEventStreamStore.cs`
|
|
- Consumer groups abstractions: `Svrnty.CQRS.Events.ConsumerGroups.Abstractions/IConsumerGroupReader.cs`, `IConsumerOffsetStore.cs`
|
|
- Consumer groups implementation: `Svrnty.CQRS.Events.ConsumerGroups/PostgresConsumerGroupReader.cs`, `PostgresConsumerOffsetStore.cs`
|
|
- Retention policy abstractions: `Svrnty.CQRS.Events.Abstractions/IRetentionPolicyStore.cs`, `IRetentionPolicy.cs`, `RetentionPolicyConfig.cs`, `RetentionCleanupResult.cs`
|
|
- Retention policy implementation: `Svrnty.CQRS.Events.PostgreSQL/PostgresRetentionPolicyStore.cs`, `RetentionPolicyService.cs`, `RetentionServiceOptions.cs`
|
|
- Event replay abstractions: `Svrnty.CQRS.Events.Abstractions/IEventReplayService.cs`, `ReplayOptions.cs`, `ReplayProgress.cs`
|
|
- Event replay implementation: `Svrnty.CQRS.Events.PostgreSQL/PostgresEventReplayService.cs`
|
|
- Stream configuration abstractions: `Svrnty.CQRS.Events.Abstractions/IStreamConfigurationStore.cs`, `IStreamConfigurationProvider.cs`, `StreamConfiguration.cs`, `RetentionConfiguration.cs`, `DeadLetterQueueConfiguration.cs`, `LifecycleConfiguration.cs`, `PerformanceConfiguration.cs`, `AccessControlConfiguration.cs`
|
|
- Stream configuration implementation: `Svrnty.CQRS.Events.PostgreSQL/PostgresStreamConfigurationStore.cs`, `PostgresStreamConfigurationProvider.cs`
|
|
- PostgreSQL migrations: `Svrnty.CQRS.Events.PostgreSQL/Migrations/003_RetentionPolicies.sql`, `Svrnty.CQRS.Events.PostgreSQL/Migrations/004_StreamConfiguration.sql`
|
|
- gRPC event streaming: `Svrnty.CQRS.Events.Grpc/EventStreamServiceImpl.cs`
|
|
- Sample application: `Svrnty.Sample/` - demonstrates both HTTP and gRPC integration
|