< Summary - AsyncResponse (Release / net8.0+net10.0 / unit+integration)

Information
Class: AsyncResponse.AsyncResponseContextPropagation
Assembly: AsyncResponse.Core
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/AsyncResponseContextPropagation.cs
Line coverage
100%
Covered lines: 83
Uncovered lines: 0
Coverable lines: 83
Total lines: 244
Line coverage: 100%
Branch coverage
96%
Covered branches: 58
Total branches: 60
Branch coverage: 96.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%22100%
Capture()100%44100%
Restore(...)100%2424100%
.cctor()100%11100%
Dispose()100%11100%
.ctor(...)100%11100%
Dispose()100%22100%
.ctor(...)100%11100%
Dispose()100%44100%
.cctor()100%11100%
.ctor(...)100%11100%
Rent(...)100%22100%
Detach()100%44100%
EnsureWritable()100%22100%
get_ReadView()100%22100%
get_Item(...)100%11100%
set_Item(...)100%11100%
get_Keys()100%11100%
get_Values()100%11100%
get_Count()100%22100%
get_IsReadOnly()100%11100%
Add(...)100%11100%
Add(...)100%11100%
ContainsKey(...)100%22100%
Contains(...)50%22100%
TryGetValue(...)100%22100%
Remove(...)100%22100%
Remove(...)100%22100%
Clear()50%22100%
CopyTo(...)100%11100%
GetEnumerator()100%11100%
System.Collections.IEnumerable.GetEnumerator()100%11100%

File(s)

/home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/AsyncResponseContextPropagation.cs

#LineLine coverage
 1using System.Collections;
 2
 3namespace AsyncResponse;
 4
 5/// <summary>
 6/// Aggregates the registered <see cref="IAsyncResponseContextPropagator"/>s into a single
 7/// capture/restore step used by the worker and lost-subscriber paths. With no propagators
 8/// registered it is a zero-overhead no-op, so the feature has no effect on apps that don't use it.
 9/// </summary>
 10internal sealed class AsyncResponseContextPropagation
 11{
 12    private readonly IReadOnlyList<IAsyncResponseContextPropagator> _propagators;
 13
 14    /// <summary>Runs the AsyncResponseContextPropagation operation.</summary>
 315    public AsyncResponseContextPropagation(IEnumerable<IAsyncResponseContextPropagator> propagators)
 316        => _propagators = propagators as IReadOnlyList<IAsyncResponseContextPropagator> ?? propagators.ToArray();
 17
 18    /// <summary>
 19    /// Captures the current ambient context from every propagator into a serializable carrier.
 20    /// Returns <c>null</c> when there are no propagators or none wrote anything, so the carrier is
 21    /// left off the wire payload entirely.
 22    /// </summary>
 23    public Dictionary<string, string>? Capture()
 24    {
 325        var propagators = _propagators;
 326        var count = propagators.Count;
 327        if (count == 0)
 328            return null;
 29
 30        // Hand each propagator a lazy carrier instead of a fully-formed Dictionary. The backing
 31        // dictionary is allocated only when a propagator actually writes a value, so the very
 32        // common "nothing to capture" path (no ambient trace/tenant/etc. set) allocates nothing.
 33        // The carrier wrapper itself is pooled per thread and never escapes this synchronous call,
 34        // so even when a propagator does write, the only surviving allocation is the dictionary
 35        // that gets returned — no wrapper and no enumerator. Indexing the list avoids the
 36        // IEnumerator<T> boxing that a foreach over an interface-typed list would incur.
 337        var carrier = LazyCapturingCarrier.Rent(count);
 338        for (var i = 0; i < count; i++)
 339            propagators[i].Capture(carrier);
 40
 341        return carrier.Detach();
 42    }
 43
 44    /// <summary>
 45    /// Restores ambient context from <paramref name="carrier"/> for the lifetime of the returned
 46    /// scope. A no-op when there are no propagators or the carrier is null/empty.
 47    /// </summary>
 48    public IDisposable Restore(IReadOnlyDictionary<string, string>? carrier)
 49    {
 350        var propagators = _propagators;
 351        var count = propagators.Count;
 352        if (count == 0 || carrier is null || carrier.Count == 0)
 353            return NullScope.Instance;
 54
 55        // Collect the real scopes without allocating until we genuinely need a composite. The 0/1/2
 56        // active-scope cases — by far the most common — return either the shared no-op, the single
 57        // scope directly, or a fixed two-field CompositeScope2. Only 3+ active propagators fall back
 58        // to the List-backed CompositeScope. Indexing the list avoids the foreach enumerator alloc.
 359        IDisposable? first = null;
 360        IDisposable? second = null;
 361        List<IDisposable>? more = null;
 62
 363        for (var i = 0; i < count; i++)
 64        {
 365            var scope = propagators[i].Restore(carrier);
 366            if (scope is null || ReferenceEquals(scope, NullScope.Instance))
 67                continue;
 68
 369            if (more is not null)
 270                more.Add(scope);
 371            else if (first is null)
 372                first = scope;
 373            else if (second is null)
 374                second = scope;
 75            else
 276                more = [first, second, scope];
 77        }
 78
 379        if (more is not null)
 280            return new CompositeScope(more);
 81
 382        if (second is not null)
 383            return new CompositeScope2(first!, second);
 84
 285        return first ?? NullScope.Instance;
 86    }
 87
 88    private sealed class NullScope : IDisposable
 89    {
 390        public static readonly NullScope Instance = new();
 91        /// <summary>Releases resources held by this instance.</summary>
 392        public void Dispose() { }
 93    }
 94
 95    /// <summary>
 96    /// Disposes exactly two scopes in reverse order, mirroring nested <c>using</c> semantics without
 97    /// the <see cref="List{T}"/> + general <see cref="CompositeScope"/> allocation that the 1–2
 98    /// propagator case (the overwhelmingly common one) would otherwise pay.
 99    /// </summary>
 3100    private sealed class CompositeScope2(IDisposable _first, IDisposable _second) : IDisposable
 101    {
 102        private int _disposed;
 103
 104        /// <summary>Releases resources held by this instance.</summary>
 105        public void Dispose()
 106        {
 3107            if (Interlocked.Exchange(ref _disposed, 1) != 0)
 2108                return;
 109
 3110            try { _second.Dispose(); }
 2111            catch { /* a misbehaving propagator scope must not mask the other */ }
 112
 3113            try { _first.Dispose(); }
 2114            catch { /* swallow: best-effort restore of the remaining scope */ }
 3115        }
 116    }
 117
 2118    private sealed class CompositeScope(List<IDisposable> _scopes) : IDisposable
 119    {
 120        private int _disposed;
 121
 122        /// <summary>Releases resources held by this instance.</summary>
 123        public void Dispose()
 124        {
 2125            if (Interlocked.Exchange(ref _disposed, 1) != 0)
 2126                return;
 127
 128            // Dispose in reverse order, mirroring nested using semantics.
 2129            for (int i = _scopes.Count - 1; i >= 0; i--)
 130            {
 2131                try { _scopes[i].Dispose(); }
 2132                catch { /* a misbehaving propagator scope must not mask the others */ }
 133            }
 2134        }
 135    }
 136
 137    /// <summary>
 138    /// An <see cref="IDictionary{TKey,TValue}"/> facade that defers creating its backing
 139    /// <see cref="Dictionary{TKey,TValue}"/> until the first mutation. Reads before any write see an
 140    /// empty map; the first write allocates a right-sized dictionary. A single instance is reused per
 141    /// thread (the carrier never escapes the synchronous <see cref="Capture"/> call), so capturing
 142    /// context adds no wrapper allocation on the hot path — only the backing dictionary, and only
 143    /// when a propagator writes.
 144    /// </summary>
 145    private sealed class LazyCapturingCarrier : IDictionary<string, string>
 146    {
 147        [ThreadStatic] private static LazyCapturingCarrier? _cached;
 148
 149        // Shared, never-mutated empty map backing every read taken before the first write.
 3150        private static readonly Dictionary<string, string> EmptyView = new(0, StringComparer.Ordinal);
 151
 152        private Dictionary<string, string>? _inner;
 153        private int _capacity;
 154
 3155        private LazyCapturingCarrier(int capacity) => _capacity = capacity;
 156
 157        /// <summary>
 158        /// Borrows the thread's pooled carrier (or allocates one on first use / under re-entrancy).
 159        /// The slot is cleared while borrowed so a re-entrant capture gets its own instance rather
 160        /// than corrupting the borrowed one.
 161        /// </summary>
 162        public static LazyCapturingCarrier Rent(int capacity)
 163        {
 3164            var carrier = _cached;
 3165            if (carrier is null)
 3166                return new LazyCapturingCarrier(capacity);
 167
 3168            _cached = null;
 3169            carrier._capacity = capacity;
 3170            return carrier;
 171        }
 172
 173        /// <summary>
 174        /// Returns the captured dictionary (or <c>null</c> when nothing was written), resets the
 175        /// carrier, and returns it to the thread pool for the next capture.
 176        /// </summary>
 177        public Dictionary<string, string>? Detach()
 178        {
 3179            var inner = _inner;
 3180            _inner = null;
 3181            _cached = this;
 3182            return inner is { Count: > 0 } ? inner : null;
 183        }
 184
 185        private Dictionary<string, string> EnsureWritable()
 3186            => _inner ??= new Dictionary<string, string>(_capacity, StringComparer.Ordinal);
 187
 2188        private Dictionary<string, string> ReadView => _inner ?? EmptyView;
 189
 190        public string this[string key]
 191        {
 2192            get => ReadView[key];
 3193            set => EnsureWritable()[key] = value;
 194        }
 195
 2196        public ICollection<string> Keys => ReadView.Keys;
 2197        public ICollection<string> Values => ReadView.Values;
 2198        public int Count => _inner?.Count ?? 0;
 2199        public bool IsReadOnly => false;
 200
 201        /// <summary>Runs the Add operation.</summary>
 2202        public void Add(string key, string value) => EnsureWritable().Add(key, value);
 203
 204        /// <summary>Runs the Add operation.</summary>
 205        public void Add(KeyValuePair<string, string> item)
 2206            => ((ICollection<KeyValuePair<string, string>>)EnsureWritable()).Add(item);
 207
 208        /// <summary>Runs the ContainsKey operation.</summary>
 2209        public bool ContainsKey(string key) => _inner?.ContainsKey(key) ?? false;
 210
 211        /// <summary>Runs the Contains operation.</summary>
 212        public bool Contains(KeyValuePair<string, string> item)
 2213            => _inner is { } inner && ((ICollection<KeyValuePair<string, string>>)inner).Contains(item);
 214
 215        /// <summary>Runs the TryGetValue operation.</summary>
 216        public bool TryGetValue(string key, out string value)
 217        {
 2218            if (_inner is { } inner)
 2219                return inner.TryGetValue(key, out value!);
 220
 2221            value = null!;
 2222            return false;
 223        }
 224
 225        /// <summary>Runs the Remove operation.</summary>
 2226        public bool Remove(string key) => _inner?.Remove(key) ?? false;
 227
 228        /// <summary>Runs the Remove operation.</summary>
 229        public bool Remove(KeyValuePair<string, string> item)
 2230            => _inner is { } inner && ((ICollection<KeyValuePair<string, string>>)inner).Remove(item);
 231
 232        /// <summary>Runs the Clear operation.</summary>
 2233        public void Clear() => _inner?.Clear();
 234
 235        /// <summary>Runs the CopyTo operation.</summary>
 236        public void CopyTo(KeyValuePair<string, string>[] array, int arrayIndex)
 2237            => ((ICollection<KeyValuePair<string, string>>)ReadView).CopyTo(array, arrayIndex);
 238
 239        /// <summary>Runs the GetEnumerator operation.</summary>
 2240        public IEnumerator<KeyValuePair<string, string>> GetEnumerator() => ReadView.GetEnumerator();
 241
 2242        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
 243    }
 244}