137 lines
4.7 KiB
C#
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 });
|
|
}
|
|
}
|
|
}
|
|
}
|