dotnet-cqrs/Svrnty.CQRS.Events/Logging/CorrelationContext.cs

84 lines
2.6 KiB
C#

using System;
using System.Threading;
namespace Svrnty.CQRS.Events.Logging;
/// <summary>
/// Manages correlation ID propagation across async operations for distributed tracing.
/// </summary>
/// <remarks>
/// <para>
/// <strong>Phase 6 Feature:</strong>
/// Uses AsyncLocal to maintain correlation ID context across async boundaries.
/// Enables full request tracing across event streams, subscriptions, and consumers.
/// </para>
/// <para>
/// <strong>Usage Pattern:</strong>
/// <code>
/// using (CorrelationContext.Begin(correlationId))
/// {
/// // All operations within this scope will have access to the correlation ID
/// await PublishEventAsync(myEvent);
/// _logger.LogEventPublished(eventId, eventType, streamName, CorrelationContext.Current);
/// }
/// </code>
/// </para>
/// </remarks>
public static class CorrelationContext
{
private static readonly AsyncLocal<string?> _correlationId = new();
/// <summary>
/// Gets the current correlation ID for this async context.
/// </summary>
/// <returns>The current correlation ID, or null if not set.</returns>
public static string? Current => _correlationId.Value;
/// <summary>
/// Begins a new correlation context with the specified ID.
/// </summary>
/// <param name="correlationId">The correlation ID to use for this context.</param>
/// <returns>A disposable scope that restores the previous correlation ID when disposed.</returns>
/// <remarks>
/// If correlationId is null, a new GUID will be generated.
/// Always use with a using statement to ensure proper cleanup.
/// </remarks>
public static IDisposable Begin(string? correlationId = null)
{
return new CorrelationScope(correlationId ?? Guid.NewGuid().ToString());
}
/// <summary>
/// Sets the correlation ID for the current async context.
/// </summary>
/// <param name="correlationId">The correlation ID to set.</param>
/// <remarks>
/// Prefer using Begin() with a using statement for automatic cleanup.
/// </remarks>
internal static void Set(string? correlationId)
{
_correlationId.Value = correlationId;
}
private sealed class CorrelationScope : IDisposable
{
private readonly string? _previousCorrelationId;
private bool _disposed;
public CorrelationScope(string correlationId)
{
_previousCorrelationId = Current;
Set(correlationId);
}
public void Dispose()
{
if (!_disposed)
{
Set(_previousCorrelationId);
_disposed = true;
}
}
}
}