using System; using Svrnty.CQRS.Events.Abstractions.EventStore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Svrnty.CQRS.Events.Abstractions; using Svrnty.CQRS.Events.Abstractions.Projections; namespace Svrnty.CQRS.Events.Projections; /// /// Extension methods for registering event sourcing projections. /// public static class ProjectionServiceCollectionExtensions { /// /// Adds projection infrastructure services to the service collection. /// /// The service collection. /// /// If true, uses in-memory checkpoint storage (for development/testing). /// If false, requires PostgresProjectionCheckpointStore or custom implementation. /// /// The service collection for chaining. public static IServiceCollection AddProjections( this IServiceCollection services, bool useInMemoryCheckpoints = false) { // Register core services services.TryAddSingleton(); services.TryAddSingleton(); // Register checkpoint store if (useInMemoryCheckpoints) { services.TryAddSingleton(); } // Register hosted service for auto-start projections services.AddHostedService(); return services; } /// /// Registers a projection with the projection engine. /// /// The type of the projection implementation. /// The type of event the projection handles. /// The service collection. /// The unique name of the projection. /// The name of the stream to consume from. /// Optional configuration for projection options. /// The service collection for chaining. /// /// /// Example: /// /// services.AddProjection<UserStatisticsProjection, UserRegisteredEvent>( /// projectionName: "user-statistics", /// streamName: "user-events", /// configure: options => /// { /// options.BatchSize = 100; /// options.AutoStart = true; /// }); /// /// /// public static IServiceCollection AddProjection( this IServiceCollection services, string projectionName, string streamName, Action? configure = null) where TProjection : class, IProjection where TEvent : ICorrelatedEvent { if (string.IsNullOrWhiteSpace(projectionName)) throw new ArgumentException("Projection name cannot be null or empty", nameof(projectionName)); if (string.IsNullOrWhiteSpace(streamName)) throw new ArgumentException("Stream name cannot be null or empty", nameof(streamName)); // Register projection implementation in DI services.TryAddScoped(); // Build options var options = new ProjectionOptions(); configure?.Invoke(options); // Register projection definition services.AddSingleton(sp => { var registry = sp.GetRequiredService(); var definition = new ProjectionDefinition { ProjectionName = projectionName, StreamName = streamName, ProjectionType = typeof(TProjection), EventType = typeof(TEvent), Options = options, Description = $"Projection '{projectionName}' consuming {typeof(TEvent).Name} from '{streamName}'" }; registry.Register(definition); return definition; }); return services; } /// /// Registers a dynamic projection that can handle multiple event types. /// /// The type of the projection implementation. /// The service collection. /// The unique name of the projection. /// The name of the stream to consume from. /// Optional configuration for projection options. /// The service collection for chaining. public static IServiceCollection AddDynamicProjection( this IServiceCollection services, string projectionName, string streamName, Action? configure = null) where TProjection : class, IDynamicProjection { if (string.IsNullOrWhiteSpace(projectionName)) throw new ArgumentException("Projection name cannot be null or empty", nameof(projectionName)); if (string.IsNullOrWhiteSpace(streamName)) throw new ArgumentException("Stream name cannot be null or empty", nameof(streamName)); // Register projection implementation in DI services.TryAddScoped(); // Build options var options = new ProjectionOptions(); configure?.Invoke(options); // Register projection definition services.AddSingleton(sp => { var registry = sp.GetRequiredService(); var definition = new ProjectionDefinition { ProjectionName = projectionName, StreamName = streamName, ProjectionType = typeof(TProjection), EventType = null, // Dynamic projections don't have a specific event type Options = options, Description = $"Dynamic projection '{projectionName}' consuming from '{streamName}'" }; registry.Register(definition); return definition; }); return services; } }