dotnet-cqrs/Svrnty.CQRS.Events.Abstractions/Models/Workflow.cs

145 lines
5.7 KiB
C#

using System;
using Svrnty.CQRS.Events.Abstractions.Models;
using System.Collections.Generic;
using System.Linq;
using Svrnty.CQRS.Events.Abstractions.EventStore;
namespace Svrnty.CQRS.Events.Abstractions.Models;
/// <summary>
/// Base class for workflows that emit correlated events.
/// A workflow represents a logical business process that may span multiple commands.
/// Each workflow instance has a unique ID that serves as the correlation ID for all events emitted within it.
/// </summary>
/// <remarks>
/// <para>
/// <strong>Design Philosophy:</strong>
/// - Workflows are the primary abstraction for event emission (events are implementation details)
/// - Each workflow instance represents a single logical process (e.g., one invitation, one order)
/// - Workflow ID becomes the correlation ID for all events
/// </para>
/// <para>
/// <strong>Developer Usage:</strong>
/// Create a workflow class by inheriting from this base class:
/// <code>
/// public class InvitationWorkflow : Workflow
/// {
/// public void EmitInvited(UserInvitedEvent e) => Emit(e);
/// public void EmitAccepted(UserInviteAcceptedEvent e) => Emit(e);
/// }
/// </code>
/// </para>
/// <para>
/// <strong>Framework Usage:</strong>
/// The framework manages workflow lifecycle:
/// - Sets <see cref="Id"/> when workflow starts or continues
/// - Sets <see cref="IsNew"/> based on whether this is a new workflow or continuation
/// - Reads <see cref="PendingEvents"/> after command execution
/// - Calls <see cref="AssignCorrelationIds"/> to set correlation IDs on all events
/// </para>
/// </remarks>
public abstract class Workflow
{
/// <summary>
/// Unique identifier for this workflow instance.
/// Set by the framework when the workflow is started or continued.
/// This ID becomes the correlation ID for all events emitted by this workflow.
/// </summary>
/// <remarks>
/// <strong>Framework Use:</strong> This property is set by the framework and should not be modified by user code.
/// </remarks>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Indicates whether this is a new workflow instance (true) or a continuation of an existing workflow (false).
/// Set by the framework based on whether the workflow was started or continued.
/// </summary>
/// <remarks>
/// This can be useful for workflow logic that should only run once (e.g., validation on start).
/// <strong>Framework Use:</strong> This property is set by the framework and should not be modified by user code.
/// </remarks>
public bool IsNew { get; set; }
/// <summary>
/// Internal collection of events that have been emitted but not yet persisted.
/// The framework reads this after command execution to emit events.
/// </summary>
private readonly List<ICorrelatedEvent> _pendingEvents = new();
/// <summary>
/// Gets the pending events that have been emitted within this workflow.
/// Used by the framework to retrieve events after command execution.
/// </summary>
/// <remarks>
/// <strong>Framework Use Only:</strong> This property is for framework use and should not be accessed by user code.
/// </remarks>
public IReadOnlyList<ICorrelatedEvent> PendingEvents => _pendingEvents.AsReadOnly();
/// <summary>
/// Emits an event within this workflow.
/// The event will be assigned this workflow's ID as its correlation ID by the framework.
/// </summary>
/// <typeparam name="TEvent">The type of event to emit. Must implement <see cref="ICorrelatedEvent"/>.</typeparam>
/// <param name="event">The event to emit.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="event"/> is null.</exception>
/// <remarks>
/// <para>
/// This method is protected so only derived workflow classes can emit events.
/// Events are collected and will be persisted by the framework after the command handler completes.
/// </para>
/// <para>
/// <strong>Usage Example:</strong>
/// <code>
/// protected void EmitInvited(UserInvitedEvent e) => Emit(e);
/// </code>
/// </para>
/// </remarks>
protected void Emit<TEvent>(TEvent @event) where TEvent : ICorrelatedEvent
{
if (@event == null)
throw new ArgumentNullException(nameof(@event));
_pendingEvents.Add(@event);
}
/// <summary>
/// Assigns this workflow's ID as the correlation ID to all pending events.
/// Called by the framework before events are persisted.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if workflow ID is not set.</exception>
/// <remarks>
/// <strong>Framework Use Only:</strong> This method is for framework use and should not be called by user code.
/// </remarks>
public void AssignCorrelationIds()
{
if (string.IsNullOrWhiteSpace(Id))
throw new InvalidOperationException("Workflow ID must be set before assigning correlation IDs.");
foreach (var @event in _pendingEvents)
{
@event.CorrelationId = Id;
}
}
/// <summary>
/// Clears all pending events.
/// Called by the framework after events have been persisted.
/// </summary>
/// <remarks>
/// <strong>Framework Use Only:</strong> This method is for framework use and should not be called by user code.
/// </remarks>
public void ClearPendingEvents()
{
_pendingEvents.Clear();
}
/// <summary>
/// Gets the number of events that have been emitted within this workflow.
/// Useful for testing and diagnostics.
/// </summary>
/// <remarks>
/// <strong>Framework Use Only:</strong> This property is for framework use and diagnostics.
/// </remarks>
public int PendingEventCount => _pendingEvents.Count;
}