| | | 1 | | namespace AsyncResponse; |
| | | 2 | | |
| | | 3 | | /// <summary> |
| | | 4 | | /// Carries ambient async-response metadata that flows with the async call stack via |
| | | 5 | | /// <see cref="AsyncLocal{T}"/>, so it is unique per logical operation. |
| | | 6 | | /// <para> |
| | | 7 | | /// Publishers fall back to this ambient value when <c>SetResponse</c>/<c>SetException</c> are |
| | | 8 | | /// called without an explicit correlation id, and worker jobs restore it before executing so |
| | | 9 | | /// downstream publishes correlate automatically. Reply targets are available to outbound |
| | | 10 | | /// integration code that needs to tell a remote system where to publish its response. |
| | | 11 | | /// Prefer passing values explicitly where practical; the ambient context exists for integration |
| | | 12 | | /// points that cannot. |
| | | 13 | | /// </para> |
| | | 14 | | /// </summary> |
| | | 15 | | public static class AsyncResponseContext |
| | | 16 | | { |
| | | 17 | | private static readonly AsyncLocal<string?> _currentCorrelationId = new(); |
| | | 18 | | private static readonly AsyncLocal<AsyncResponseReplyTarget?> _currentReplyTarget = new(); |
| | | 19 | | |
| | | 20 | | /// <summary>Gets the correlation id of the current logical operation, if any.</summary> |
| | | 21 | | public static string? CorrelationId => _currentCorrelationId.Value; |
| | | 22 | | |
| | | 23 | | /// <summary>Gets the reply target selected for the current logical operation, if any.</summary> |
| | | 24 | | public static AsyncResponseReplyTarget? ReplyTarget => _currentReplyTarget.Value; |
| | | 25 | | |
| | | 26 | | /// <summary>Generates a new correlation id, stores it in the ambient context, and returns it.</summary> |
| | | 27 | | public static string CreateCorrelationId() |
| | | 28 | | { |
| | | 29 | | var correlationId = GenerateCorrelationId(); |
| | | 30 | | _currentCorrelationId.Value = correlationId; |
| | | 31 | | return correlationId; |
| | | 32 | | } |
| | | 33 | | |
| | | 34 | | /// <summary>Generates a new correlation id without storing it.</summary> |
| | | 35 | | public static string GenerateCorrelationId() => Guid.NewGuid().ToString(); |
| | | 36 | | |
| | | 37 | | /// <summary>Ensures the ambient correlation id is non-empty, generating one if missing.</summary> |
| | | 38 | | public static string EnsureCorrelationId() |
| | | 39 | | { |
| | | 40 | | if (string.IsNullOrWhiteSpace(_currentCorrelationId.Value)) |
| | | 41 | | { |
| | | 42 | | _currentCorrelationId.Value = GenerateCorrelationId(); |
| | | 43 | | } |
| | | 44 | | |
| | | 45 | | return _currentCorrelationId.Value!; |
| | | 46 | | } |
| | | 47 | | |
| | | 48 | | /// <summary>Sets the ambient correlation id for the current logical operation.</summary> |
| | | 49 | | /// <exception cref="ArgumentException">Thrown when <paramref name="correlationId"/> is null or whitespace.</excepti |
| | | 50 | | public static void SetCorrelationId(string correlationId) |
| | | 51 | | { |
| | | 52 | | _currentCorrelationId.Value = !string.IsNullOrWhiteSpace(correlationId) |
| | | 53 | | ? correlationId |
| | | 54 | | : throw new ArgumentException("CorrelationId cannot be null or whitespace.", nameof(correlationId)); |
| | | 55 | | } |
| | | 56 | | |
| | | 57 | | /// <summary>Sets the ambient reply target for the current logical operation.</summary> |
| | | 58 | | public static void SetReplyTarget(AsyncResponseReplyTarget replyTarget) |
| | | 59 | | { |
| | | 60 | | ArgumentNullException.ThrowIfNull(replyTarget); |
| | | 61 | | ValidateReplyTarget(replyTarget); |
| | | 62 | | _currentReplyTarget.Value = replyTarget; |
| | | 63 | | } |
| | | 64 | | |
| | | 65 | | /// <summary> |
| | | 66 | | /// Temporarily sets the ambient correlation id for the current logical operation and |
| | | 67 | | /// restores the previous value when the returned scope is disposed. Passing <c>null</c> or |
| | | 68 | | /// whitespace clears the ambient id for the scope. |
| | | 69 | | /// </summary> |
| | | 70 | | internal static IDisposable PushCorrelationId(string? correlationId) |
| | | 71 | | { |
| | | 72 | | var previousCorrelationId = _currentCorrelationId.Value; |
| | | 73 | | _currentCorrelationId.Value = !string.IsNullOrWhiteSpace(correlationId) ? correlationId : null; |
| | | 74 | | return new CorrelationScope(previousCorrelationId); |
| | | 75 | | } |
| | | 76 | | |
| | | 77 | | /// <summary> |
| | | 78 | | /// Temporarily sets the ambient async-response context and restores the previous values when |
| | | 79 | | /// the returned scope is disposed. |
| | | 80 | | /// </summary> |
| | | 81 | | internal static IDisposable PushContext(string? correlationId, AsyncResponseReplyTarget? replyTarget) |
| | | 82 | | { |
| | | 83 | | if (replyTarget is not null) |
| | | 84 | | { |
| | | 85 | | ValidateReplyTarget(replyTarget); |
| | | 86 | | } |
| | | 87 | | |
| | | 88 | | var previousCorrelationId = _currentCorrelationId.Value; |
| | | 89 | | var previousReplyTarget = _currentReplyTarget.Value; |
| | | 90 | | |
| | | 91 | | _currentCorrelationId.Value = !string.IsNullOrWhiteSpace(correlationId) ? correlationId : null; |
| | | 92 | | _currentReplyTarget.Value = replyTarget; |
| | | 93 | | |
| | | 94 | | return new ContextScope(previousCorrelationId, previousReplyTarget); |
| | | 95 | | } |
| | | 96 | | |
| | | 97 | | /// <summary>Clears the ambient correlation id for the current logical operation.</summary> |
| | | 98 | | public static void ClearCorrelationId() => _currentCorrelationId.Value = null; |
| | | 99 | | |
| | | 100 | | /// <summary>Clears the ambient reply target for the current logical operation.</summary> |
| | | 101 | | public static void ClearReplyTarget() => _currentReplyTarget.Value = null; |
| | | 102 | | |
| | | 103 | | private static void ValidateReplyTarget(AsyncResponseReplyTarget replyTarget) |
| | | 104 | | { |
| | | 105 | | ArgumentException.ThrowIfNullOrWhiteSpace(replyTarget.Name); |
| | | 106 | | ArgumentException.ThrowIfNullOrWhiteSpace(replyTarget.Transport); |
| | | 107 | | ArgumentException.ThrowIfNullOrWhiteSpace(replyTarget.Address); |
| | | 108 | | } |
| | | 109 | | |
| | | 110 | | private sealed class CorrelationScope(string? _previousCorrelationId) : IDisposable |
| | | 111 | | { |
| | | 112 | | private int _disposed; |
| | | 113 | | |
| | | 114 | | /// <summary>Releases resources held by this instance.</summary> |
| | | 115 | | public void Dispose() |
| | | 116 | | { |
| | | 117 | | if (Interlocked.Exchange(ref _disposed, 1) == 0) |
| | | 118 | | { |
| | | 119 | | _currentCorrelationId.Value = _previousCorrelationId; |
| | | 120 | | } |
| | | 121 | | } |
| | | 122 | | } |
| | | 123 | | |
| | 3 | 124 | | private sealed class ContextScope( |
| | 3 | 125 | | string? _previousCorrelationId, |
| | 3 | 126 | | AsyncResponseReplyTarget? _previousReplyTarget) : IDisposable |
| | | 127 | | { |
| | | 128 | | private int _disposed; |
| | | 129 | | |
| | | 130 | | /// <summary>Releases resources held by this instance.</summary> |
| | | 131 | | public void Dispose() |
| | | 132 | | { |
| | 3 | 133 | | if (Interlocked.Exchange(ref _disposed, 1) == 0) |
| | | 134 | | { |
| | 3 | 135 | | _currentCorrelationId.Value = _previousCorrelationId; |
| | 3 | 136 | | _currentReplyTarget.Value = _previousReplyTarget; |
| | | 137 | | } |
| | 3 | 138 | | } |
| | | 139 | | } |
| | | 140 | | } |