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 });
}
}
}
}