dotnet-cqrs/Svrnty.CQRS.Events/Projections/ProjectionServiceCollectionExtensions.cs

165 lines
6.2 KiB
C#

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;
/// <summary>
/// Extension methods for registering event sourcing projections.
/// </summary>
public static class ProjectionServiceCollectionExtensions
{
/// <summary>
/// Adds projection infrastructure services to the service collection.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="useInMemoryCheckpoints">
/// If true, uses in-memory checkpoint storage (for development/testing).
/// If false, requires PostgresProjectionCheckpointStore or custom implementation.
/// </param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddProjections(
this IServiceCollection services,
bool useInMemoryCheckpoints = false)
{
// Register core services
services.TryAddSingleton<IProjectionRegistry, ProjectionRegistry>();
services.TryAddSingleton<IProjectionEngine, ProjectionEngine>();
// Register checkpoint store
if (useInMemoryCheckpoints)
{
services.TryAddSingleton<IProjectionCheckpointStore, InMemoryProjectionCheckpointStore>();
}
// Register hosted service for auto-start projections
services.AddHostedService<ProjectionHostedService>();
return services;
}
/// <summary>
/// Registers a projection with the projection engine.
/// </summary>
/// <typeparam name="TProjection">The type of the projection implementation.</typeparam>
/// <typeparam name="TEvent">The type of event the projection handles.</typeparam>
/// <param name="services">The service collection.</param>
/// <param name="projectionName">The unique name of the projection.</param>
/// <param name="streamName">The name of the stream to consume from.</param>
/// <param name="configure">Optional configuration for projection options.</param>
/// <returns>The service collection for chaining.</returns>
/// <remarks>
/// <para>
/// <strong>Example:</strong>
/// <code>
/// services.AddProjection&lt;UserStatisticsProjection, UserRegisteredEvent&gt;(
/// projectionName: "user-statistics",
/// streamName: "user-events",
/// configure: options =>
/// {
/// options.BatchSize = 100;
/// options.AutoStart = true;
/// });
/// </code>
/// </para>
/// </remarks>
public static IServiceCollection AddProjection<TProjection, TEvent>(
this IServiceCollection services,
string projectionName,
string streamName,
Action<ProjectionOptions>? configure = null)
where TProjection : class, IProjection<TEvent>
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<TProjection>();
// Build options
var options = new ProjectionOptions();
configure?.Invoke(options);
// Register projection definition
services.AddSingleton(sp =>
{
var registry = sp.GetRequiredService<IProjectionRegistry>();
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;
}
/// <summary>
/// Registers a dynamic projection that can handle multiple event types.
/// </summary>
/// <typeparam name="TProjection">The type of the projection implementation.</typeparam>
/// <param name="services">The service collection.</param>
/// <param name="projectionName">The unique name of the projection.</param>
/// <param name="streamName">The name of the stream to consume from.</param>
/// <param name="configure">Optional configuration for projection options.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddDynamicProjection<TProjection>(
this IServiceCollection services,
string projectionName,
string streamName,
Action<ProjectionOptions>? 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<TProjection>();
// Build options
var options = new ProjectionOptions();
configure?.Invoke(options);
// Register projection definition
services.AddSingleton(sp =>
{
var registry = sp.GetRequiredService<IProjectionRegistry>();
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;
}
}