693 lines
30 KiB
C#
693 lines
30 KiB
C#
using System;
|
|
using Svrnty.CQRS.Events.Metrics;
|
|
using Svrnty.CQRS.Events.Decorators;
|
|
using Svrnty.CQRS.Events.Abstractions.Streaming;
|
|
using Svrnty.CQRS.Events.HealthCheck;
|
|
using Svrnty.CQRS.Events.Configuration;
|
|
using Svrnty.CQRS.Events.Abstractions.Delivery;
|
|
using Svrnty.CQRS.Events.Abstractions.Schema;
|
|
using Svrnty.CQRS.Events.Abstractions.Storage;
|
|
using Svrnty.CQRS.Events.Abstractions.Correlation;
|
|
using Svrnty.CQRS.Events.Delivery;
|
|
using Svrnty.CQRS.Events.InMemory;
|
|
using Svrnty.CQRS.Events.Schema;
|
|
using Svrnty.CQRS.Events.Core;
|
|
using Svrnty.CQRS.Events.Subscriptions;
|
|
using Svrnty.CQRS.Events.Abstractions.Subscriptions;
|
|
using Svrnty.CQRS.Events.Abstractions.EventHandlers;
|
|
using Svrnty.CQRS.Events.Abstractions.Models;
|
|
using Svrnty.CQRS.Events.Abstractions.EventStore;
|
|
using FluentValidation;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Svrnty.CQRS.Abstractions;
|
|
using Svrnty.CQRS.Abstractions.Discovery;
|
|
using Svrnty.CQRS.Events.Abstractions;
|
|
using Svrnty.CQRS.Events.Abstractions.Discovery;
|
|
using Svrnty.CQRS.Events.Discovery;
|
|
using Svrnty.CQRS.Events.Services;
|
|
using Svrnty.CQRS.Events.Storage;
|
|
using InMemoryEventStreamStore = Svrnty.CQRS.Events.Storage.InMemoryEventStreamStore;
|
|
using InMemoryConsumerRegistry = Svrnty.CQRS.Events.Storage.InMemoryConsumerRegistry;
|
|
using InMemorySubscriptionStore = Svrnty.CQRS.Events.Storage.InMemorySubscriptionStore;
|
|
using EventDeliveryService = Svrnty.CQRS.Events.Delivery.EventDeliveryService;
|
|
|
|
namespace Svrnty.CQRS.Events;
|
|
|
|
/// <summary>
|
|
/// Service collection extensions for registering event services.
|
|
/// </summary>
|
|
public static class ServiceCollectionExtensions
|
|
{
|
|
/// <summary>
|
|
/// Add core event services (emitter, subscription service, delivery service).
|
|
/// Note: Storage implementations (IEventStore, ISubscriptionStore) must be registered separately.
|
|
/// </summary>
|
|
public static IServiceCollection AddSvrntyEvents(this IServiceCollection services)
|
|
{
|
|
services.TryAddTransient<IEventEmitter, EventEmitter>();
|
|
services.TryAddTransient<IEventSubscriptionService, EventSubscriptionService>();
|
|
services.TryAddTransient<IEventDeliveryService, EventDeliveryService>();
|
|
|
|
// Phase 1.4: Subscription client for consuming events
|
|
services.TryAddSingleton<IEventSubscriptionClient, EventSubscriptionClient>();
|
|
|
|
// Phase 3.2: Exactly-once delivery decorator
|
|
services.TryAddSingleton<ExactlyOnceDeliveryDecorator>();
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add default event discovery service.
|
|
/// </summary>
|
|
public static IServiceCollection AddDefaultEventDiscovery(this IServiceCollection services)
|
|
{
|
|
services.TryAddTransient<IEventDiscovery, EventDiscovery>();
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register an event type for discovery.
|
|
/// This allows the event to be discovered by IEventDiscovery and code generators.
|
|
/// </summary>
|
|
public static IServiceCollection AddEvent<TEvent>(
|
|
this IServiceCollection services,
|
|
string? description = null)
|
|
where TEvent : class, ICorrelatedEvent
|
|
{
|
|
var eventType = typeof(TEvent);
|
|
var meta = new EventMeta(eventType, description);
|
|
|
|
services.AddSingleton<IEventMeta>(meta);
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add in-memory storage implementations for events and subscriptions.
|
|
/// Suitable for development and testing. Data is lost on application restart.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <strong>Registered Services:</strong>
|
|
/// - <see cref="IEventStore"/> - Legacy persistent event storage
|
|
/// - <see cref="IEventStreamStore"/> - New stream-based storage (Phase 1.3)
|
|
/// - <see cref="ISubscriptionStore"/> - Subscription configuration storage
|
|
/// - <see cref="ICorrelationStore"/> - Correlation ID tracking
|
|
/// - <see cref="IConsumerRegistry"/> - Active consumer tracking (Phase 1.3)
|
|
/// </para>
|
|
/// </remarks>
|
|
public static IServiceCollection AddInMemoryEventStorage(this IServiceCollection services)
|
|
{
|
|
// Legacy event storage (persistent)
|
|
services.TryAddSingleton<IEventStore, InMemoryEventStore>();
|
|
|
|
// Phase 1.3: Stream-based storage (ephemeral)
|
|
services.TryAddSingleton<IEventStreamStore, InMemoryEventStreamStore>();
|
|
|
|
// Subscription and consumer management
|
|
services.TryAddSingleton<ISubscriptionStore, InMemorySubscriptionStore>();
|
|
services.TryAddSingleton<IConsumerRegistry, InMemoryConsumerRegistry>();
|
|
|
|
// Correlation tracking
|
|
services.TryAddSingleton<ICorrelationStore, InMemoryCorrelationStore>();
|
|
|
|
// Phase 3.1: Idempotency store for exactly-once delivery
|
|
services.TryAddSingleton<IIdempotencyStore, InMemoryIdempotencyStore>();
|
|
|
|
// Phase 3.3: Read receipt store for consumer progress tracking
|
|
services.TryAddSingleton<IReadReceiptStore, InMemoryReadReceiptStore>();
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register a command handler that automatically manages event emission with correlation IDs.
|
|
/// Events are strongly-typed via the TEvents base type or marker interface.
|
|
/// </summary>
|
|
/// <typeparam name="TCommand">The command type.</typeparam>
|
|
/// <typeparam name="TResult">The result type.</typeparam>
|
|
/// <typeparam name="TEvents">The base type or marker interface for events this command can emit.</typeparam>
|
|
/// <typeparam name="THandler">The handler type implementing ICommandHandlerWithEvents.</typeparam>
|
|
public static IServiceCollection AddCommandWithEvents<TCommand, TResult, TEvents, THandler>(this IServiceCollection services)
|
|
where TCommand : class
|
|
where TEvents : ICorrelatedEvent
|
|
where THandler : class, ICommandHandlerWithEvents<TCommand, TResult, TEvents>
|
|
{
|
|
// Register the actual handler
|
|
services.AddTransient<THandler>();
|
|
|
|
// Register metadata for discovery
|
|
var commandMeta = new CommandMeta(typeof(TCommand), typeof(THandler), typeof(TResult));
|
|
services.AddSingleton<ICommandMeta>(commandMeta);
|
|
|
|
// Register the decorator as the ICommandHandler that the framework uses
|
|
services.AddTransient<ICommandHandler<TCommand, TResult>>(sp =>
|
|
{
|
|
var handler = sp.GetRequiredService<THandler>();
|
|
var eventEmitter = sp.GetRequiredService<IEventEmitter>();
|
|
var correlationStore = sp.GetService<ICorrelationStore>(); // Optional
|
|
return new CommandHandlerWithEventsDecorator<TCommand, TResult, TEvents>(handler, eventEmitter, correlationStore);
|
|
});
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register a command handler with FluentValidation that automatically manages event emission.
|
|
/// </summary>
|
|
/// <typeparam name="TCommand">The command type.</typeparam>
|
|
/// <typeparam name="TResult">The result type.</typeparam>
|
|
/// <typeparam name="TEvents">The base type or marker interface for events this command can emit.</typeparam>
|
|
/// <typeparam name="THandler">The handler type implementing ICommandHandlerWithEvents.</typeparam>
|
|
/// <typeparam name="TValidator">The FluentValidation validator type.</typeparam>
|
|
public static IServiceCollection AddCommandWithEvents<TCommand, TResult, TEvents, THandler, TValidator>(this IServiceCollection services)
|
|
where TCommand : class
|
|
where TEvents : ICorrelatedEvent
|
|
where THandler : class, ICommandHandlerWithEvents<TCommand, TResult, TEvents>
|
|
where TValidator : class, IValidator<TCommand>
|
|
{
|
|
// Register validator
|
|
services.AddTransient<IValidator<TCommand>, TValidator>();
|
|
|
|
// Register command with events
|
|
return services.AddCommandWithEvents<TCommand, TResult, TEvents, THandler>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register a command handler (no result) that automatically manages event emission.
|
|
/// Events are strongly-typed via the TEvents base type or marker interface.
|
|
/// </summary>
|
|
/// <typeparam name="TCommand">The command type.</typeparam>
|
|
/// <typeparam name="TEvents">The base type or marker interface for events this command can emit.</typeparam>
|
|
/// <typeparam name="THandler">The handler type implementing ICommandHandlerWithEvents.</typeparam>
|
|
public static IServiceCollection AddCommandWithEvents<TCommand, TEvents, THandler>(this IServiceCollection services)
|
|
where TCommand : class
|
|
where TEvents : ICorrelatedEvent
|
|
where THandler : class, ICommandHandlerWithEvents<TCommand, TEvents>
|
|
{
|
|
// Register the actual handler
|
|
services.AddTransient<THandler>();
|
|
|
|
// Register metadata for discovery
|
|
var commandMeta = new CommandMeta(typeof(TCommand), typeof(THandler));
|
|
services.AddSingleton<ICommandMeta>(commandMeta);
|
|
|
|
// Register the decorator as the ICommandHandler that the framework uses
|
|
services.AddTransient<ICommandHandler<TCommand>>(sp =>
|
|
{
|
|
var handler = sp.GetRequiredService<THandler>();
|
|
var eventEmitter = sp.GetRequiredService<IEventEmitter>();
|
|
var correlationStore = sp.GetService<ICorrelationStore>(); // Optional
|
|
return new CommandHandlerWithEventsDecoratorNoResult<TCommand, TEvents>(handler, eventEmitter, correlationStore);
|
|
});
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register a command handler that returns both the result and correlation ID.
|
|
/// Use this for multi-step workflows where the correlation ID needs to be returned to the caller
|
|
/// so it can be passed to follow-up commands.
|
|
/// </summary>
|
|
/// <typeparam name="TCommand">The command type.</typeparam>
|
|
/// <typeparam name="TResult">The result type.</typeparam>
|
|
/// <typeparam name="TEvents">The base type or marker interface for events this command can emit.</typeparam>
|
|
/// <typeparam name="THandler">The handler type implementing ICommandHandlerWithEventsAndCorrelation.</typeparam>
|
|
public static IServiceCollection AddCommandWithEventsAndCorrelation<TCommand, TResult, TEvents, THandler>(this IServiceCollection services)
|
|
where TCommand : class
|
|
where TEvents : ICorrelatedEvent
|
|
where THandler : class, ICommandHandlerWithEventsAndCorrelation<TCommand, TResult, TEvents>
|
|
{
|
|
// Register the actual handler
|
|
services.AddTransient<THandler>();
|
|
|
|
// Register metadata for discovery (with the wrapped result type)
|
|
var commandMeta = new CommandMeta(typeof(TCommand), typeof(THandler), typeof(CommandResultWithCorrelation<TResult>));
|
|
services.AddSingleton<ICommandMeta>(commandMeta);
|
|
|
|
// Register the decorator as the ICommandHandler that the framework uses
|
|
services.AddTransient<ICommandHandler<TCommand, CommandResultWithCorrelation<TResult>>>(sp =>
|
|
{
|
|
var handler = sp.GetRequiredService<THandler>();
|
|
var eventEmitter = sp.GetRequiredService<IEventEmitter>();
|
|
var correlationStore = sp.GetService<ICorrelationStore>(); // Optional
|
|
return new CommandHandlerWithEventsAndCorrelationDecorator<TCommand, TResult, TEvents>(handler, eventEmitter, correlationStore);
|
|
});
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register a command handler that returns both the result and correlation ID, with FluentValidation support.
|
|
/// Use this for multi-step workflows where the correlation ID needs to be returned to the caller.
|
|
/// </summary>
|
|
/// <typeparam name="TCommand">The command type.</typeparam>
|
|
/// <typeparam name="TResult">The result type.</typeparam>
|
|
/// <typeparam name="TEvents">The base type or marker interface for events this command can emit.</typeparam>
|
|
/// <typeparam name="THandler">The handler type implementing ICommandHandlerWithEventsAndCorrelation.</typeparam>
|
|
/// <typeparam name="TValidator">The FluentValidation validator type.</typeparam>
|
|
public static IServiceCollection AddCommandWithEventsAndCorrelation<TCommand, TResult, TEvents, THandler, TValidator>(this IServiceCollection services)
|
|
where TCommand : class
|
|
where TEvents : ICorrelatedEvent
|
|
where THandler : class, ICommandHandlerWithEventsAndCorrelation<TCommand, TResult, TEvents>
|
|
where TValidator : class, IValidator<TCommand>
|
|
{
|
|
// Register validator
|
|
services.AddTransient<IValidator<TCommand>, TValidator>();
|
|
|
|
// Register command with events and correlation
|
|
return services.AddCommandWithEventsAndCorrelation<TCommand, TResult, TEvents, THandler>();
|
|
}
|
|
|
|
// ============================================================================
|
|
// WORKFLOW-BASED COMMAND REGISTRATION (NEW API)
|
|
// ============================================================================
|
|
|
|
/// <summary>
|
|
/// Register a command handler that participates in a workflow and returns a result.
|
|
/// The workflow manages event emission and correlation automatically.
|
|
/// </summary>
|
|
/// <typeparam name="TCommand">The command type.</typeparam>
|
|
/// <typeparam name="TResult">The result type.</typeparam>
|
|
/// <typeparam name="TWorkflow">The workflow type that manages events. Must inherit from <see cref="Workflow"/> and have a parameterless constructor.</typeparam>
|
|
/// <typeparam name="THandler">The handler type implementing ICommandHandlerWithWorkflow.</typeparam>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <strong>Workflow Pattern (Recommended):</strong>
|
|
/// This is the recommended way to register commands that emit events. Workflows provide:
|
|
/// - Clearer business process modeling (workflow = business process)
|
|
/// - Automatic correlation ID management (workflow ID = correlation ID)
|
|
/// - Type-safe event emission within workflow boundaries
|
|
/// - Foundation for multi-step workflows and event sourcing
|
|
/// </para>
|
|
/// <para>
|
|
/// <strong>Example Usage:</strong>
|
|
/// <code>
|
|
/// services.AddCommandWithWorkflow<InviteUserCommand, string, InvitationWorkflow, InviteUserCommandHandler>();
|
|
/// </code>
|
|
/// </para>
|
|
/// <para>
|
|
/// <strong>Phase 1 Behavior:</strong>
|
|
/// - Each command execution creates a new workflow instance
|
|
/// - Workflow ID is auto-generated (GUID)
|
|
/// - All events emitted within the handler receive the workflow ID as correlation ID
|
|
/// </para>
|
|
/// <para>
|
|
/// <strong>Future Phases:</strong>
|
|
/// Later phases will add workflow continuation (resume existing workflows by ID) and
|
|
/// persistent workflow state for event sourcing scenarios.
|
|
/// </para>
|
|
/// </remarks>
|
|
public static IServiceCollection AddCommandWithWorkflow<TCommand, TResult, TWorkflow, THandler>(this IServiceCollection services)
|
|
where TCommand : class
|
|
where TWorkflow : Workflow, new()
|
|
where THandler : class, ICommandHandlerWithWorkflow<TCommand, TResult, TWorkflow>
|
|
{
|
|
// Register the actual handler
|
|
services.AddTransient<THandler>();
|
|
|
|
// Register metadata for discovery
|
|
var commandMeta = new CommandMeta(typeof(TCommand), typeof(THandler), typeof(TResult));
|
|
services.AddSingleton<ICommandMeta>(commandMeta);
|
|
|
|
// Register the decorator as the ICommandHandler that the framework uses
|
|
services.AddTransient<ICommandHandler<TCommand, TResult>>(sp =>
|
|
{
|
|
var handler = sp.GetRequiredService<THandler>();
|
|
var eventEmitter = sp.GetRequiredService<IEventEmitter>();
|
|
return new CommandHandlerWithWorkflowDecorator<TCommand, TResult, TWorkflow>(handler, eventEmitter);
|
|
});
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register a command handler that participates in a workflow, returns a result, and includes FluentValidation support.
|
|
/// </summary>
|
|
/// <typeparam name="TCommand">The command type.</typeparam>
|
|
/// <typeparam name="TResult">The result type.</typeparam>
|
|
/// <typeparam name="TWorkflow">The workflow type that manages events. Must inherit from <see cref="Workflow"/> and have a parameterless constructor.</typeparam>
|
|
/// <typeparam name="THandler">The handler type implementing ICommandHandlerWithWorkflow.</typeparam>
|
|
/// <typeparam name="TValidator">The FluentValidation validator type.</typeparam>
|
|
/// <remarks>
|
|
/// This is a convenience overload that registers both the validator and the workflow command.
|
|
/// Equivalent to calling AddTransient<IValidator<TCommand>, TValidator>() followed by AddCommandWithWorkflow().
|
|
/// </remarks>
|
|
public static IServiceCollection AddCommandWithWorkflow<TCommand, TResult, TWorkflow, THandler, TValidator>(this IServiceCollection services)
|
|
where TCommand : class
|
|
where TWorkflow : Workflow, new()
|
|
where THandler : class, ICommandHandlerWithWorkflow<TCommand, TResult, TWorkflow>
|
|
where TValidator : class, IValidator<TCommand>
|
|
{
|
|
// Register validator
|
|
services.AddTransient<IValidator<TCommand>, TValidator>();
|
|
|
|
// Register command with workflow
|
|
return services.AddCommandWithWorkflow<TCommand, TResult, TWorkflow, THandler>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register a command handler that participates in a workflow but does not return a result.
|
|
/// The workflow manages event emission and correlation automatically.
|
|
/// </summary>
|
|
/// <typeparam name="TCommand">The command type.</typeparam>
|
|
/// <typeparam name="TWorkflow">The workflow type that manages events. Must inherit from <see cref="Workflow"/> and have a parameterless constructor.</typeparam>
|
|
/// <typeparam name="THandler">The handler type implementing ICommandHandlerWithWorkflow.</typeparam>
|
|
/// <remarks>
|
|
/// This is the "no result" variant of <see cref="AddCommandWithWorkflow{TCommand, TResult, TWorkflow, THandler}(IServiceCollection)"/>.
|
|
/// Use this when your command performs an action but doesn't need to return a value.
|
|
/// </remarks>
|
|
public static IServiceCollection AddCommandWithWorkflow<TCommand, TWorkflow, THandler>(this IServiceCollection services)
|
|
where TCommand : class
|
|
where TWorkflow : Workflow, new()
|
|
where THandler : class, ICommandHandlerWithWorkflow<TCommand, TWorkflow>
|
|
{
|
|
// Register the actual handler
|
|
services.AddTransient<THandler>();
|
|
|
|
// Register metadata for discovery (no result type)
|
|
var commandMeta = new CommandMeta(typeof(TCommand), typeof(THandler));
|
|
services.AddSingleton<ICommandMeta>(commandMeta);
|
|
|
|
// Register the decorator as the ICommandHandler that the framework uses
|
|
services.AddTransient<ICommandHandler<TCommand>>(sp =>
|
|
{
|
|
var handler = sp.GetRequiredService<THandler>();
|
|
var eventEmitter = sp.GetRequiredService<IEventEmitter>();
|
|
return new CommandHandlerWithWorkflowDecoratorNoResult<TCommand, TWorkflow>(handler, eventEmitter);
|
|
});
|
|
|
|
return services;
|
|
}
|
|
|
|
// ============================================================================
|
|
// EVENT STREAMING CONFIGURATION API (FLUENT BUILDER)
|
|
// ============================================================================
|
|
|
|
/// <summary>
|
|
/// Configures event streaming services using a fluent API.
|
|
/// </summary>
|
|
/// <param name="services">The service collection to configure.</param>
|
|
/// <param name="configure">Action to configure streaming options using the fluent builder.</param>
|
|
/// <returns>The service collection for method chaining.</returns>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <strong>Purpose:</strong>
|
|
/// This method provides a centralized way to configure all event streaming aspects including
|
|
/// streams, subscriptions, delivery options, and storage implementations.
|
|
/// </para>
|
|
/// <para>
|
|
/// <strong>Phase 1 Usage:</strong>
|
|
/// <code>
|
|
/// services.AddEventStreaming(streaming =>
|
|
/// {
|
|
/// // Configure a stream
|
|
/// streaming.AddStream<UserWorkflow>(stream =>
|
|
/// {
|
|
/// stream.Type = StreamType.Ephemeral;
|
|
/// stream.DeliverySemantics = DeliverySemantics.AtLeastOnce;
|
|
/// });
|
|
///
|
|
/// // Configure a subscription
|
|
/// streaming.AddSubscription<UserWorkflow>("analytics", sub =>
|
|
/// {
|
|
/// sub.Mode = SubscriptionMode.Broadcast;
|
|
/// });
|
|
/// });
|
|
/// </code>
|
|
/// </para>
|
|
/// <para>
|
|
/// <strong>Progressive Complexity:</strong>
|
|
/// Additional configuration options (subscriptions, external delivery, schema evolution)
|
|
/// will be added to the builder in later phases, maintaining backward compatibility.
|
|
/// </para>
|
|
/// </remarks>
|
|
public static IServiceCollection AddEventStreaming(
|
|
this IServiceCollection services,
|
|
Action<EventStreamingBuilder>? configure = null)
|
|
{
|
|
var builder = new EventStreamingBuilder(services);
|
|
|
|
// Invoke user configuration
|
|
configure?.Invoke(builder);
|
|
|
|
// Subscriptions are now automatically registered via EventSubscriptionClient constructor
|
|
// which receives IEnumerable<Subscription> from DI
|
|
|
|
return services;
|
|
}
|
|
|
|
// ========================================================================
|
|
// Phase 3: Exactly-Once Delivery & Read Receipt Configuration
|
|
// ========================================================================
|
|
|
|
/// <summary>
|
|
/// Configure exactly-once delivery options.
|
|
/// </summary>
|
|
/// <param name="services">The service collection.</param>
|
|
/// <param name="configure">Configuration action for exactly-once delivery options.</param>
|
|
/// <returns>The service collection for method chaining.</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// services.ConfigureExactlyOnceDelivery(options =>
|
|
/// {
|
|
/// options.LockDuration = TimeSpan.FromSeconds(60);
|
|
/// options.MaxRetries = 5;
|
|
/// options.RetryDelay = TimeSpan.FromMilliseconds(200);
|
|
/// options.UseExponentialBackoff = true;
|
|
/// });
|
|
/// </code>
|
|
/// </example>
|
|
public static IServiceCollection ConfigureExactlyOnceDelivery(
|
|
this IServiceCollection services,
|
|
Action<ExactlyOnceDeliveryOptions> configure)
|
|
{
|
|
if (services == null)
|
|
throw new ArgumentNullException(nameof(services));
|
|
if (configure == null)
|
|
throw new ArgumentNullException(nameof(configure));
|
|
|
|
services.Configure(configure);
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configure read receipt tracking and cleanup options.
|
|
/// </summary>
|
|
/// <param name="services">The service collection.</param>
|
|
/// <param name="configure">Configuration action for read receipt options.</param>
|
|
/// <returns>The service collection for method chaining.</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// services.ConfigureReadReceipts(options =>
|
|
/// {
|
|
/// options.EnableAutoCleanup = true;
|
|
/// options.CleanupInterval = TimeSpan.FromHours(6);
|
|
/// options.RetentionPeriod = TimeSpan.FromDays(60);
|
|
/// });
|
|
/// </code>
|
|
/// </example>
|
|
public static IServiceCollection ConfigureReadReceipts(
|
|
this IServiceCollection services,
|
|
Action<ReadReceiptOptions> configure)
|
|
{
|
|
if (services == null)
|
|
throw new ArgumentNullException(nameof(services));
|
|
if (configure == null)
|
|
throw new ArgumentNullException(nameof(configure));
|
|
|
|
services.Configure(configure);
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add read receipt cleanup background service.
|
|
/// </summary>
|
|
/// <param name="services">The service collection.</param>
|
|
/// <returns>The service collection for method chaining.</returns>
|
|
/// <remarks>
|
|
/// The cleanup service will run based on the configured ReadReceiptOptions.
|
|
/// If EnableAutoCleanup is false, the service will not perform any cleanup.
|
|
/// </remarks>
|
|
public static IServiceCollection AddReadReceiptCleanup(this IServiceCollection services)
|
|
{
|
|
if (services == null)
|
|
throw new ArgumentNullException(nameof(services));
|
|
|
|
services.AddHostedService<ReadReceiptCleanupService>();
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add idempotency cleanup background service.
|
|
/// </summary>
|
|
/// <param name="services">The service collection.</param>
|
|
/// <returns>The service collection for method chaining.</returns>
|
|
/// <remarks>
|
|
/// The cleanup service will periodically remove old processed event records
|
|
/// and expired idempotency locks.
|
|
/// </remarks>
|
|
public static IServiceCollection AddIdempotencyCleanup(this IServiceCollection services)
|
|
{
|
|
if (services == null)
|
|
throw new ArgumentNullException(nameof(services));
|
|
|
|
services.AddHostedService<IdempotencyCleanupService>();
|
|
return services;
|
|
}
|
|
|
|
// ========================================================================
|
|
// Phase 5: Schema Evolution & Versioning
|
|
// ========================================================================
|
|
|
|
/// <summary>
|
|
/// Adds schema evolution support with in-memory schema storage.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <strong>Registered Services:</strong>
|
|
/// - <see cref="ISchemaRegistry"/> - Schema registration and upcasting
|
|
/// - <see cref="ISchemaStore"/> - In-memory schema storage
|
|
/// </para>
|
|
/// <para>
|
|
/// For production use with persistent storage, use <see cref="AddPostgresSchemaStore"/>
|
|
/// or implement a custom <see cref="ISchemaStore"/>.
|
|
/// </para>
|
|
/// </remarks>
|
|
public static IServiceCollection AddSchemaEvolution(this IServiceCollection services)
|
|
{
|
|
if (services == null)
|
|
throw new ArgumentNullException(nameof(services));
|
|
|
|
// Register schema store (in-memory by default)
|
|
services.TryAddSingleton<ISchemaStore, InMemorySchemaStore>();
|
|
|
|
// Register schema registry
|
|
services.TryAddSingleton<ISchemaRegistry, SchemaRegistry>();
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds JSON Schema generation support for event versioning.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// This is optional. If registered, the schema registry will automatically
|
|
/// generate JSON schemas for registered event types.
|
|
/// </para>
|
|
/// <para>
|
|
/// JSON schemas enable:
|
|
/// - External consumers (non-.NET) to understand event structure
|
|
/// - Schema validation
|
|
/// - Documentation generation
|
|
/// - Code generation for other languages
|
|
/// </para>
|
|
/// </remarks>
|
|
public static IServiceCollection AddJsonSchemaGeneration(this IServiceCollection services)
|
|
{
|
|
if (services == null)
|
|
throw new ArgumentNullException(nameof(services));
|
|
|
|
services.TryAddSingleton<IJsonSchemaGenerator, SystemTextJsonSchemaGenerator>();
|
|
|
|
return services;
|
|
}
|
|
|
|
// ========================================================================
|
|
// Phase 6: Management, Monitoring & Observability
|
|
// ========================================================================
|
|
|
|
/// <summary>
|
|
/// Adds stream health check services (Phase 6).
|
|
/// </summary>
|
|
/// <param name="services">The service collection.</param>
|
|
/// <param name="configure">Optional configuration action for health check options.</param>
|
|
/// <returns>The service collection for chaining.</returns>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <strong>Registered Services:</strong>
|
|
/// - <see cref="IStreamHealthCheck"/> - Stream and subscription health checking
|
|
/// - <see cref="StreamHealthCheckOptions"/> - Configurable thresholds for lag and staleness
|
|
/// </para>
|
|
/// <para>
|
|
/// <strong>Health Check Capabilities:</strong>
|
|
/// - Stream availability and writability
|
|
/// - Consumer lag detection (events behind stream head)
|
|
/// - Stalled consumer detection (no progress for N minutes)
|
|
/// - Configurable degraded/unhealthy thresholds
|
|
/// </para>
|
|
/// </remarks>
|
|
public static IServiceCollection AddStreamHealthChecks(
|
|
this IServiceCollection services,
|
|
Action<StreamHealthCheckOptions>? configure = null)
|
|
{
|
|
if (services == null)
|
|
throw new ArgumentNullException(nameof(services));
|
|
|
|
if (configure != null)
|
|
{
|
|
services.Configure(configure);
|
|
}
|
|
else
|
|
{
|
|
services.Configure<StreamHealthCheckOptions>(_ => { });
|
|
}
|
|
|
|
services.TryAddSingleton<IStreamHealthCheck, StreamHealthCheck>();
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds event stream metrics and telemetry (Phase 6).
|
|
/// </summary>
|
|
/// <param name="services">The service collection.</param>
|
|
/// <returns>The service collection for chaining.</returns>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <strong>Registered Services:</strong>
|
|
/// - <see cref="IEventStreamMetrics"/> - Metrics collection for event streaming
|
|
/// </para>
|
|
/// <para>
|
|
/// <strong>Collected Metrics:</strong>
|
|
/// - Events published per stream (counter with stream/event_type tags)
|
|
/// - Events consumed per subscription (counter with stream/subscription/event_type tags)
|
|
/// - Processing latency (histogram with stream/subscription tags)
|
|
/// - Consumer lag (gauge with stream/subscription tags)
|
|
/// - Error rate (counter with stream/subscription/error_type tags)
|
|
/// - Retry count (counter with stream/subscription/attempt tags)
|
|
/// - Stream length (gauge with stream tag)
|
|
/// - Active consumers (gauge with stream/subscription tags)
|
|
/// </para>
|
|
/// <para>
|
|
/// <strong>OpenTelemetry Integration:</strong>
|
|
/// This implementation uses .NET's System.Diagnostics.Metrics API which is
|
|
/// automatically discovered by OpenTelemetry instrumentation. To export metrics:
|
|
/// <code>
|
|
/// services.AddOpenTelemetry()
|
|
/// .WithMetrics(builder => builder
|
|
/// .AddMeter("Svrnty.CQRS.Events")
|
|
/// .AddPrometheusExporter());
|
|
/// </code>
|
|
/// </para>
|
|
/// <para>
|
|
/// <strong>Prometheus Integration:</strong>
|
|
/// Use OpenTelemetry's Prometheus exporter to expose metrics at /metrics endpoint.
|
|
/// All metrics use the prefix "svrnty_cqrs_events_" in Prometheus format.
|
|
/// </para>
|
|
/// </remarks>
|
|
public static IServiceCollection AddEventStreamMetrics(this IServiceCollection services)
|
|
{
|
|
if (services == null)
|
|
throw new ArgumentNullException(nameof(services));
|
|
|
|
services.TryAddSingleton<IEventStreamMetrics, EventStreamMetrics>();
|
|
|
|
return services;
|
|
}
|
|
}
|