dotnet-cqrs/CLAUDE.md

40 KiB

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

# 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:

// 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:

The Svrnty.CQRS.Grpc package with Svrnty.CQRS.Grpc.Generators source generator provides high-performance gRPC endpoints:

Registration:

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

The Svrnty.CQRS.MinimalApi package provides HTTP endpoints for CQRS commands and queries:

Registration:

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:

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:

// 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:

# 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 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:

// 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:

// 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:

// 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:

// 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:

// 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:

// 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:

# 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:

// 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:

# 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:

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:

// 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