235 lines
8.7 KiB
C#
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<UserWorkflow>(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<string> { "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);
|
|
}
|
|
}
|