dotnet-cqrs/Svrnty.CQRS.Events/Core/EventStreamingBuilder.cs

235 lines
8.7 KiB
C#

using System;
using Svrnty.CQRS.Events.Abstractions.Streaming;
using Svrnty.CQRS.Events.Subscriptions;
using Svrnty.CQRS.Events.Configuration;
using Svrnty.CQRS.Events.Abstractions.Subscriptions;
using Svrnty.CQRS.Events.Abstractions.Models;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Svrnty.CQRS.Events.Abstractions;
namespace Svrnty.CQRS.Events.Core;
/// <summary>
/// Builder for configuring event streaming services using a fluent API.
/// </summary>
/// <remarks>
/// <para>
/// The <see cref="EventStreamingBuilder"/> provides a fluent interface for configuring
/// event streams, subscriptions, and delivery options. This builder is returned by
/// <see cref="ServiceCollectionExtensions.AddEventStreaming(IServiceCollection, Action{EventStreamingBuilder})"/>
/// and allows for progressive configuration as features are added in each phase.
/// </para>
/// <para>
/// <strong>Phase 1 Focus:</strong>
/// Basic stream configuration with workflow-based event emission.
/// Additional configuration options will be added in later phases.
/// </para>
/// </remarks>
public class EventStreamingBuilder
{
private readonly IServiceCollection _services;
private readonly Dictionary<string, IStreamConfiguration> _streamConfigurations = new();
/// <summary>
/// Initializes a new instance of the <see cref="EventStreamingBuilder"/> class.
/// </summary>
/// <param name="services">The service collection to configure.</param>
internal EventStreamingBuilder(IServiceCollection services)
{
_services = services ?? throw new ArgumentNullException(nameof(services));
}
/// <summary>
/// Gets the service collection being configured.
/// </summary>
public IServiceCollection Services => _services;
/// <summary>
/// Adds a stream configuration for a specific workflow.
/// </summary>
/// <typeparam name="TWorkflow">The workflow type that emits events to this stream.</typeparam>
/// <param name="configure">Optional action to configure stream settings.</param>
/// <returns>The builder for method chaining.</returns>
/// <remarks>
/// <para>
/// <strong>Phase 1 Behavior:</strong>
/// Creates an ephemeral stream with at-least-once delivery by default.
/// Stream name is derived from the workflow type name.
/// </para>
/// <para>
/// <strong>Example Usage:</strong>
/// <code>
/// streaming.AddStream&lt;UserWorkflow&gt;(stream =>
/// {
/// stream.Type = StreamType.Persistent;
/// stream.DeliverySemantics = DeliverySemantics.ExactlyOnce;
/// });
/// </code>
/// </para>
/// </remarks>
public EventStreamingBuilder AddStream<TWorkflow>(Action<IStreamConfiguration>? configure = null)
where TWorkflow : Workflow
{
var streamName = GetStreamName<TWorkflow>();
var config = new StreamConfiguration(streamName);
configure?.Invoke(config);
config.Validate();
_streamConfigurations[streamName] = config;
// Register the configuration as a singleton
_services.AddSingleton(config);
return this;
}
/// <summary>
/// Adds a stream configuration with an explicit stream name.
/// </summary>
/// <param name="streamName">The name of the stream.</param>
/// <param name="configure">Optional action to configure stream settings.</param>
/// <returns>The builder for method chaining.</returns>
/// <remarks>
/// Use this overload when you need explicit control over stream naming,
/// or when configuring streams that aren't associated with a specific workflow type.
/// </remarks>
public EventStreamingBuilder AddStream(string streamName, Action<IStreamConfiguration>? configure = null)
{
if (string.IsNullOrWhiteSpace(streamName))
throw new ArgumentException("Stream name cannot be null or whitespace.", nameof(streamName));
var config = new StreamConfiguration(streamName);
configure?.Invoke(config);
config.Validate();
_streamConfigurations[streamName] = config;
// Register the configuration as a singleton
_services.AddSingleton(config);
return this;
}
/// <summary>
/// Gets the stream name for a workflow type.
/// </summary>
/// <typeparam name="TWorkflow">The workflow type.</typeparam>
/// <returns>The stream name derived from the workflow type.</returns>
/// <remarks>
/// <strong>Naming Convention:</strong>
/// - Removes "Workflow" suffix if present
/// - Converts to kebab-case
/// - Appends "-events"
/// <para>
/// Examples:
/// - UserWorkflow → "user-events"
/// - InvitationWorkflow → "invitation-events"
/// - OrderProcessing → "order-processing-events"
/// </para>
/// </remarks>
private static string GetStreamName<TWorkflow>() where TWorkflow : Workflow
{
var typeName = typeof(TWorkflow).Name;
// Remove "Workflow" suffix if present
if (typeName.EndsWith("Workflow", StringComparison.OrdinalIgnoreCase))
{
typeName = typeName.Substring(0, typeName.Length - "Workflow".Length);
}
// Convert to kebab-case (simplified version for now)
var kebabCase = System.Text.RegularExpressions.Regex.Replace(
typeName,
"(?<!^)([A-Z])",
"-$1"
).ToLowerInvariant();
return $"{kebabCase}-events";
}
/// <summary>
/// Gets all registered stream configurations.
/// Used internally by the framework.
/// </summary>
internal IReadOnlyDictionary<string, IStreamConfiguration> GetStreamConfigurations()
{
return _streamConfigurations;
}
// ========================================================================
// SUBSCRIPTION CONFIGURATION (Phase 1.4)
// ========================================================================
/// <summary>
/// Adds a subscription configuration for consuming events from a stream.
/// </summary>
/// <param name="subscriptionId">Unique subscription identifier.</param>
/// <param name="streamName">Name of the stream to subscribe to.</param>
/// <param name="configure">Optional action to configure subscription settings.</param>
/// <returns>The builder for method chaining.</returns>
/// <remarks>
/// <para>
/// <strong>Phase 1 Behavior:</strong>
/// Creates a subscription with Broadcast mode by default.
/// Subscription is registered with the EventSubscriptionClient for consumption.
/// </para>
/// <para>
/// <strong>Example Usage:</strong>
/// <code>
/// streaming.AddSubscription("analytics", "user-events", sub =>
/// {
/// sub.Mode = SubscriptionMode.Exclusive;
/// sub.VisibilityTimeout = TimeSpan.FromSeconds(60);
/// sub.EventTypeFilter = new HashSet&lt;string&gt; { "UserAddedEvent", "UserRemovedEvent" };
/// });
/// </code>
/// </para>
/// </remarks>
public EventStreamingBuilder AddSubscription(
string subscriptionId,
string streamName,
Action<Subscription>? configure = null)
{
if (string.IsNullOrWhiteSpace(subscriptionId))
throw new ArgumentException("Subscription ID cannot be null or whitespace.", nameof(subscriptionId));
if (string.IsNullOrWhiteSpace(streamName))
throw new ArgumentException("Stream name cannot be null or whitespace.", nameof(streamName));
var subscription = new Subscription(subscriptionId, streamName);
configure?.Invoke(subscription);
subscription.Validate();
// Register the subscription with the client
// (We'll get the client from DI when the builder is executed)
_services.AddSingleton(subscription);
// Configure the subscription when the service provider is built
_services.AddSingleton<ISubscription>(subscription);
return this;
}
/// <summary>
/// Adds a subscription for a specific workflow stream.
/// </summary>
/// <typeparam name="TWorkflow">The workflow type whose events to subscribe to.</typeparam>
/// <param name="subscriptionId">Unique subscription identifier.</param>
/// <param name="configure">Optional action to configure subscription settings.</param>
/// <returns>The builder for method chaining.</returns>
/// <remarks>
/// Convenience method that automatically derives the stream name from the workflow type.
/// </remarks>
public EventStreamingBuilder AddSubscription<TWorkflow>(
string subscriptionId,
Action<Subscription>? configure = null)
where TWorkflow : Workflow
{
var streamName = GetStreamName<TWorkflow>();
return AddSubscription(subscriptionId, streamName, configure);
}
}