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; /// /// Implementation of event context that collects events for batch emission. /// Used internally by the framework to manage event collection and correlation. /// /// The base type or marker interface for events. internal sealed class EventContext : IEventContext where TEvents : ICorrelatedEvent { private readonly ICorrelationStore? _correlationStore; private readonly List _events = new(); private string? _loadedKeyHash; private bool _correlationLoaded; public EventContext(ICorrelationStore? correlationStore = null) { _correlationStore = correlationStore; } /// /// Gets all collected events. /// public IReadOnlyList Events => _events.AsReadOnly(); /// /// Correlation ID that will be assigned to all events. /// Set by the framework before event emission. /// public string? CorrelationId { get; set; } /// /// Whether correlation ID was loaded from business data. /// public bool IsCorrelationLoaded => _correlationLoaded; /// /// Hash of the correlation key used to load the correlation ID. /// public string? LoadedKeyHash => _loadedKeyHash; /// /// Load or create correlation ID based on business data. /// public async Task LoadAsync(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; } /// /// Emit an event. The event is collected and will be persisted by the framework /// after the command handler completes. /// public void Emit(TEvents @event) { if (@event == null) throw new ArgumentNullException(nameof(@event)); _events.Add(@event); } /// /// Hash the correlation key to create a stable identifier. /// private static string HashCorrelationKey(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); } /// /// Assigns the correlation ID to all collected events. /// Called by the framework after the handler completes. /// 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 }); } } } }