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

137 lines
4.7 KiB
C#

using System;
using Svrnty.CQRS.Events.Abstractions.Context;
using Svrnty.CQRS.Events.Abstractions.Correlation;
using Svrnty.CQRS.Events.Abstractions.EventStore;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Svrnty.CQRS.Events.Abstractions;
namespace Svrnty.CQRS.Events.Core;
/// <summary>
/// Implementation of event context that collects events for batch emission.
/// Used internally by the framework to manage event collection and correlation.
/// </summary>
/// <typeparam name="TEvents">The base type or marker interface for events.</typeparam>
internal sealed class EventContext<TEvents> : IEventContext<TEvents>
where TEvents : ICorrelatedEvent
{
private readonly ICorrelationStore? _correlationStore;
private readonly List<ICorrelatedEvent> _events = new();
private string? _loadedKeyHash;
private bool _correlationLoaded;
public EventContext(ICorrelationStore? correlationStore = null)
{
_correlationStore = correlationStore;
}
/// <summary>
/// Gets all collected events.
/// </summary>
public IReadOnlyList<ICorrelatedEvent> Events => _events.AsReadOnly();
/// <summary>
/// Correlation ID that will be assigned to all events.
/// Set by the framework before event emission.
/// </summary>
public string? CorrelationId { get; set; }
/// <summary>
/// Whether correlation ID was loaded from business data.
/// </summary>
public bool IsCorrelationLoaded => _correlationLoaded;
/// <summary>
/// Hash of the correlation key used to load the correlation ID.
/// </summary>
public string? LoadedKeyHash => _loadedKeyHash;
/// <summary>
/// Load or create correlation ID based on business data.
/// </summary>
public async Task LoadAsync<TCorrelationKey>(TCorrelationKey correlationKey, CancellationToken cancellationToken = default)
{
if (correlationKey == null)
throw new ArgumentNullException(nameof(correlationKey));
if (_correlationStore == null)
throw new InvalidOperationException("ICorrelationStore is not configured. Add correlation store to DI.");
// Hash the correlation key to create stable identifier
_loadedKeyHash = HashCorrelationKey(correlationKey);
// Try to load existing correlation ID
var existingCorrelationId = await _correlationStore.GetCorrelationIdAsync(_loadedKeyHash, cancellationToken);
if (existingCorrelationId != null)
{
// Use existing correlation ID
CorrelationId = existingCorrelationId;
}
// If not found, correlation ID will be generated by decorator and stored
_correlationLoaded = true;
}
/// <summary>
/// Emit an event. The event is collected and will be persisted by the framework
/// after the command handler completes.
/// </summary>
public void Emit(TEvents @event)
{
if (@event == null)
throw new ArgumentNullException(nameof(@event));
_events.Add(@event);
}
/// <summary>
/// Hash the correlation key to create a stable identifier.
/// </summary>
private static string HashCorrelationKey<TCorrelationKey>(TCorrelationKey key)
{
// Serialize to JSON for stable hashing
var json = JsonSerializer.Serialize(key, new JsonSerializerOptions
{
WriteIndented = false,
PropertyNamingPolicy = null // Keep original casing
});
// Hash using SHA256
var bytes = Encoding.UTF8.GetBytes(json);
var hash = SHA256.HashData(bytes);
// Convert to base64 for storage
return Convert.ToBase64String(hash);
}
/// <summary>
/// Assigns the correlation ID to all collected events.
/// Called by the framework after the handler completes.
/// </summary>
public void AssignCorrelationIds(string correlationId)
{
CorrelationId = correlationId;
foreach (var @event in _events)
{
// Use reflection to set the correlation ID
var correlationIdProperty = @event.GetType().GetProperty(nameof(ICorrelatedEvent.CorrelationId));
if (correlationIdProperty != null && correlationIdProperty.CanWrite)
{
correlationIdProperty.SetValue(@event, correlationId);
}
else if (correlationIdProperty != null && correlationIdProperty.GetSetMethod(nonPublic: true) != null)
{
// Handle init-only properties
correlationIdProperty.GetSetMethod(nonPublic: true)!.Invoke(@event, new object[] { correlationId });
}
}
}
}