File: src\libraries\System.Private.CoreLib\src\System\Threading\AsyncLocal.cs
Web Access
Project: src\src\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj (System.Private.CoreLib)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
 
namespace System.Threading
{
    /// <summary>Represents ambient data that is local to a given asynchronous control flow, such as an asynchronous method.</summary>
    /// <typeparam name="T">The type of the ambient data.</typeparam>
    public sealed class AsyncLocal<T> : IAsyncLocal
    {
        private readonly Action<AsyncLocalValueChangedArgs<T>>? _valueChangedHandler;
 
        /// <summary>Instantiates an <see cref="AsyncLocal{T}"/> instance that does not receive change notifications.</summary>
        public AsyncLocal()
        {
        }
 
        /// <summary>Instantiates an <see cref="AsyncLocal{T}"/> instance that receives change notifications.</summary>
        /// <param name="valueChangedHandler">The delegate that is called whenever the current value changes on any thread.</param>
        public AsyncLocal(Action<AsyncLocalValueChangedArgs<T>>? valueChangedHandler)
        {
            _valueChangedHandler = valueChangedHandler;
        }
 
        /// <summary>Gets or sets the value of the ambient data.</summary>
        /// <value>The value of the ambient data. If no value has been set, the returned value is default(T).</value>
        [MaybeNull]
        public T Value
        {
            get
            {
                object? value = ExecutionContext.GetLocalValue(this);
                if (typeof(T).IsValueType && value is null)
                {
                    return default;
                }
 
                return (T)value!;
            }
            set
            {
                ExecutionContext.SetLocalValue(this, value, _valueChangedHandler is not null);
            }
        }
 
        void IAsyncLocal.OnValueChanged(object? previousValueObj, object? currentValueObj, bool contextChanged)
        {
            Debug.Assert(_valueChangedHandler is not null);
            T previousValue = previousValueObj is null ? default! : (T)previousValueObj;
            T currentValue = currentValueObj is null ? default! : (T)currentValueObj;
            _valueChangedHandler(new AsyncLocalValueChangedArgs<T>(previousValue, currentValue, contextChanged));
        }
    }
 
    /// <summary>Interface to allow non-generic code in ExecutionContext to call into the generic <see cref="AsyncLocal{T}"/> type.</summary>
    internal interface IAsyncLocal
    {
        void OnValueChanged(object? previousValue, object? currentValue, bool contextChanged);
    }
 
    /// <summary>The class that provides data change information to <see cref="AsyncLocal{T}"/> instances that register for change notifications.</summary>
    /// <typeparam name="T">The type of the data.</typeparam>
    public readonly struct AsyncLocalValueChangedArgs<T>
    {
        /// <summary>Gets the data's previous value.</summary>
        public T? PreviousValue { get; }
 
        /// <summary>Gets the data's current value.</summary>
        public T? CurrentValue { get; }
 
        /// <summary>Returns a value that indicates whether the value changes because of a change of execution context.</summary>
        /// <value>true if the value changed because of a change of execution context; otherwise, false.</value>
        public bool ThreadContextChanged { get; }
 
        internal AsyncLocalValueChangedArgs(T? previousValue, T? currentValue, bool contextChanged)
        {
            PreviousValue = previousValue!;
            CurrentValue = currentValue!;
            ThreadContextChanged = contextChanged;
        }
    }
 
    /// <summary>
    /// Interface used to store an IAsyncLocal => object mapping in ExecutionContext.
    /// Implementations are specialized based on the number of elements in the immutable
    /// map in order to minimize memory consumption and look-up times.
    /// </summary>
    internal interface IAsyncLocalValueMap
    {
        bool TryGetValue(IAsyncLocal key, out object? value);
        IAsyncLocalValueMap Set(IAsyncLocal key, object? value, bool treatNullValueAsNonexistent);
    }
 
    /// <summary>Utility functions for getting/creating instances of IAsyncLocalValueMap</summary>
    internal static class AsyncLocalValueMap
    {
        public static IAsyncLocalValueMap Empty { get; } = new EmptyAsyncLocalValueMap();
 
        public static bool IsEmpty(IAsyncLocalValueMap asyncLocalValueMap)
        {
            Debug.Assert(asyncLocalValueMap is not null);
            Debug.Assert(asyncLocalValueMap == Empty || asyncLocalValueMap.GetType() != typeof(EmptyAsyncLocalValueMap));
 
            return asyncLocalValueMap == Empty;
        }
 
        public static IAsyncLocalValueMap Create(IAsyncLocal key, object? value, bool treatNullValueAsNonexistent)
        {
            // If the value isn't null or a null value may not be treated as nonexistent, then create a new one-element map
            // to store the key/value pair.  Otherwise, use the empty map.
            return value is not null || !treatNullValueAsNonexistent ?
                new OneElementAsyncLocalValueMap(KeyValuePair.Create(key, value)) :
                Empty;
        }
 
        // Instance without any key/value pairs.  Used as a singleton/
        private sealed class EmptyAsyncLocalValueMap : IAsyncLocalValueMap
        {
            public IAsyncLocalValueMap Set(IAsyncLocal key, object? value, bool treatNullValueAsNonexistent)
            {
                // If the value isn't null or a null value may not be treated as nonexistent, then create a new one-element map
                // to store the key/value pair.  Otherwise, use the empty map.
                return value is not null || !treatNullValueAsNonexistent ?
                    new OneElementAsyncLocalValueMap(KeyValuePair.Create(key, value)) :
                    this;
            }
 
            public bool TryGetValue(IAsyncLocal key, out object? value)
            {
                value = null;
                return false;
            }
        }
 
        /// <summary>Instance with one key/value pair.</summary>
        private sealed class OneElementAsyncLocalValueMap : IAsyncLocalValueMap
        {
            private readonly KeyValuePair<IAsyncLocal, object?> _item0;
 
            public OneElementAsyncLocalValueMap(KeyValuePair<IAsyncLocal, object?> item0)
            {
                _item0 = item0;
            }
 
            public IAsyncLocalValueMap Set(IAsyncLocal key, object? value, bool treatNullValueAsNonexistent)
            {
                if (value is not null || !treatNullValueAsNonexistent)
                {
                    // If the key matches one already contained in this map, then create a new one-element map with the updated
                    // value, otherwise create a two-element map with the additional key/value.
                    KeyValuePair<IAsyncLocal, object?> newItem = KeyValuePair.Create(key, value);
                    return ReferenceEquals(key, _item0.Key) ?
                        new OneElementAsyncLocalValueMap(newItem) :
                        new TwoElementAsyncLocalValueMap(_item0, newItem);
                }
                else
                {
                    // If the key exists in this map, remove it by downgrading to an empty map.  Otherwise, there's nothing to
                    // add or remove, so just return this map.
                    return ReferenceEquals(key, _item0.Key) ?
                        Empty :
                        this;
                }
            }
 
            public bool TryGetValue(IAsyncLocal key, out object? value)
            {
                if (ReferenceEquals(key, _item0.Key))
                {
                    value = _item0.Value;
                    return true;
                }
                else
                {
                    value = null;
                    return false;
                }
            }
        }
 
        /// <summary>Instance with two key/value pairs.</summary>
        private sealed class TwoElementAsyncLocalValueMap : IAsyncLocalValueMap
        {
            private readonly KeyValuePair<IAsyncLocal, object?> _item0;
            private readonly KeyValuePair<IAsyncLocal, object?> _item1;
 
            public TwoElementAsyncLocalValueMap(
                KeyValuePair<IAsyncLocal, object?> item0,
                KeyValuePair<IAsyncLocal, object?> item1)
            {
                _item0 = item0;
                _item1 = item1;
            }
 
            public IAsyncLocalValueMap Set(IAsyncLocal key, object? value, bool treatNullValueAsNonexistent)
            {
                if (value is not null || !treatNullValueAsNonexistent)
                {
                    // If the key matches one already contained in this map, then create a new two-element map with the updated
                    // value, otherwise create a three-element map with the additional key/value.
                    KeyValuePair<IAsyncLocal, object?> newItem = KeyValuePair.Create(key, value);
                    return
                        ReferenceEquals(key, _item0.Key) ? new TwoElementAsyncLocalValueMap(newItem, _item1) :
                        ReferenceEquals(key, _item1.Key) ? new TwoElementAsyncLocalValueMap(_item0, newItem) :
                        new ThreeElementAsyncLocalValueMap(_item0, _item1, newItem);
                }
                else
                {
                    // If the key exists in this map, remove it by downgrading to a one-element map without the key.  Otherwise,
                    // there's nothing to add or remove, so just return this map.
                    return
                        ReferenceEquals(key, _item0.Key) ? new OneElementAsyncLocalValueMap(_item1) :
                        ReferenceEquals(key, _item1.Key) ? new OneElementAsyncLocalValueMap(_item0) :
                        this;
                }
            }
 
            public bool TryGetValue(IAsyncLocal key, out object? value)
            {
                if (ReferenceEquals(key, _item0.Key))
                {
                    value = _item0.Value;
                    return true;
                }
                else if (ReferenceEquals(key, _item1.Key))
                {
                    value = _item1.Value;
                    return true;
                }
                else
                {
                    value = null;
                    return false;
                }
            }
        }
 
        /// <summary>Instance with three key/value pairs.</summary>
        private sealed class ThreeElementAsyncLocalValueMap : IAsyncLocalValueMap
        {
            private readonly KeyValuePair<IAsyncLocal, object?> _item0;
            private readonly KeyValuePair<IAsyncLocal, object?> _item1;
            private readonly KeyValuePair<IAsyncLocal, object?> _item2;
 
            public ThreeElementAsyncLocalValueMap(
                KeyValuePair<IAsyncLocal, object?> item0,
                KeyValuePair<IAsyncLocal, object?> item1,
                KeyValuePair<IAsyncLocal, object?> item2)
            {
                _item0 = item0;
                _item1 = item1;
                _item2 = item2;
            }
 
            public IAsyncLocalValueMap Set(IAsyncLocal key, object? value, bool treatNullValueAsNonexistent)
            {
                if (value is not null || !treatNullValueAsNonexistent)
                {
                    // If the key matches one already contained in this map, then create a new three-element map with the updated
                    // value, otherwise create a three-element map with the additional key/value.
                    KeyValuePair<IAsyncLocal, object?> newItem = KeyValuePair.Create(key, value);
                    return
                        ReferenceEquals(key, _item0.Key) ? new ThreeElementAsyncLocalValueMap(newItem, _item1, _item2) :
                        ReferenceEquals(key, _item1.Key) ? new ThreeElementAsyncLocalValueMap(_item0, newItem, _item2) :
                        ReferenceEquals(key, _item2.Key) ? new ThreeElementAsyncLocalValueMap(_item0, _item1, newItem) :
                        new FourElementAsyncLocalValueMap(_item0, _item1, _item2, newItem);
                }
                else
                {
                    // If the key exists in this map, remove it by downgrading to a one-element map without the key.  Otherwise,
                    // there's nothing to add or remove, so just return this map.
                    return
                        ReferenceEquals(key, _item0.Key) ? new TwoElementAsyncLocalValueMap(_item1, _item2) :
                        ReferenceEquals(key, _item1.Key) ? new TwoElementAsyncLocalValueMap(_item0, _item2) :
                        ReferenceEquals(key, _item2.Key) ? new TwoElementAsyncLocalValueMap(_item0, _item1) :
                        this;
                }
            }
 
            public bool TryGetValue(IAsyncLocal key, out object? value)
            {
                if (ReferenceEquals(key, _item0.Key))
                {
                    value = _item0.Value;
                    return true;
                }
                else if (ReferenceEquals(key, _item1.Key))
                {
                    value = _item1.Value;
                    return true;
                }
                else if (ReferenceEquals(key, _item2.Key))
                {
                    value = _item2.Value;
                    return true;
                }
                else
                {
                    value = null;
                    return false;
                }
            }
        }
 
        /// <summary>Instance with four key/value pairs.</summary>
        private sealed class FourElementAsyncLocalValueMap : IAsyncLocalValueMap
        {
            private readonly KeyValuePair<IAsyncLocal, object?> _item0;
            private readonly KeyValuePair<IAsyncLocal, object?> _item1;
            private readonly KeyValuePair<IAsyncLocal, object?> _item2;
            private readonly KeyValuePair<IAsyncLocal, object?> _item3;
 
            public FourElementAsyncLocalValueMap(
                KeyValuePair<IAsyncLocal, object?> item0,
                KeyValuePair<IAsyncLocal, object?> item1,
                KeyValuePair<IAsyncLocal, object?> item2,
                KeyValuePair<IAsyncLocal, object?> item3)
            {
                _item0 = item0;
                _item1 = item1;
                _item2 = item2;
                _item3 = item3;
            }
 
            public IAsyncLocalValueMap Set(IAsyncLocal key, object? value, bool treatNullValueAsNonexistent)
            {
                if (value is not null || !treatNullValueAsNonexistent)
                {
                    // If the key matches one already contained in this map, then create a new four-element map with the updated value.
                    KeyValuePair<IAsyncLocal, object?> newItem = KeyValuePair.Create(key, value);
                    return
                        ReferenceEquals(key, _item0.Key) ? new FourElementAsyncLocalValueMap(newItem, _item1, _item2, _item3) :
                        ReferenceEquals(key, _item1.Key) ? new FourElementAsyncLocalValueMap(_item0, newItem, _item2, _item3) :
                        ReferenceEquals(key, _item2.Key) ? new FourElementAsyncLocalValueMap(_item0, _item1, newItem, _item3) :
                        ReferenceEquals(key, _item3.Key) ? new FourElementAsyncLocalValueMap(_item0, _item1, _item2, newItem) :
                        new MultiElementAsyncLocalValueMap(new[] { _item0, _item1, _item2, _item3, newItem }); // upgrade to a multi
                }
                else
                {
                    // If the key exists in this map, remove it by downgrading to a two-element map without the key.  Otherwise,
                    // there's nothing to add or remove, so just return this map.
                    return
                        ReferenceEquals(key, _item0.Key) ? new ThreeElementAsyncLocalValueMap(_item1, _item2, _item3) :
                        ReferenceEquals(key, _item1.Key) ? new ThreeElementAsyncLocalValueMap(_item0, _item2, _item3) :
                        ReferenceEquals(key, _item2.Key) ? new ThreeElementAsyncLocalValueMap(_item0, _item1, _item3) :
                        ReferenceEquals(key, _item3.Key) ? new ThreeElementAsyncLocalValueMap(_item0, _item1, _item2) :
                        this;
                }
            }
 
            public bool TryGetValue(IAsyncLocal key, out object? value)
            {
                if (ReferenceEquals(key, _item0.Key))
                {
                    value = _item0.Value;
                    return true;
                }
                else if (ReferenceEquals(key, _item1.Key))
                {
                    value = _item1.Value;
                    return true;
                }
                else if (ReferenceEquals(key, _item2.Key))
                {
                    value = _item2.Value;
                    return true;
                }
                else if (ReferenceEquals(key, _item3.Key))
                {
                    value = _item3.Value;
                    return true;
                }
                else
                {
                    value = null;
                    return false;
                }
            }
        }
 
        /// <summary>Instance with up to 16 key/value pairs.</summary>
        private sealed class MultiElementAsyncLocalValueMap : IAsyncLocalValueMap
        {
            internal const int MaxMultiElements = 16;
            private readonly KeyValuePair<IAsyncLocal, object?>[] _keyValues;
 
            internal MultiElementAsyncLocalValueMap(KeyValuePair<IAsyncLocal, object?>[] keyValues)
            {
                Debug.Assert(keyValues.Length is >= 5 and <= MaxMultiElements);
                _keyValues = keyValues;
            }
 
            public IAsyncLocalValueMap Set(IAsyncLocal key, object? value, bool treatNullValueAsNonexistent)
            {
                // Find the key in this map.
                for (int i = 0; i < _keyValues.Length; i++)
                {
                    if (ReferenceEquals(key, _keyValues[i].Key))
                    {
                        // The key is in the map.
                        if (value is not null || !treatNullValueAsNonexistent)
                        {
                            // Create a new map of the same size that has all of the same pairs, with this new key/value pair overwriting the old.
                            KeyValuePair<IAsyncLocal, object?>[] newValues = _keyValues.AsSpan().ToArray();
                            newValues[i] = KeyValuePair.Create(key, value);
                            return new MultiElementAsyncLocalValueMap(newValues);
                        }
                        else if (_keyValues.Length == 5)
                        {
                            // We only have five elements, one of which we're removing, so downgrade to a four-element map,
                            // without the matching element.
                            return i switch
                            {
                                0 => new FourElementAsyncLocalValueMap(_keyValues[1], _keyValues[2], _keyValues[3], _keyValues[4]),
                                1 => new FourElementAsyncLocalValueMap(_keyValues[0], _keyValues[2], _keyValues[3], _keyValues[4]),
                                2 => new FourElementAsyncLocalValueMap(_keyValues[0], _keyValues[1], _keyValues[3], _keyValues[4]),
                                3 => new FourElementAsyncLocalValueMap(_keyValues[0], _keyValues[1], _keyValues[2], _keyValues[4]),
                                _ => new FourElementAsyncLocalValueMap(_keyValues[0], _keyValues[1], _keyValues[2], _keyValues[3]),
                            };
                        }
                        else
                        {
                            // We have enough elements remaining to warrant a multi map.  Create a new one and copy all of the
                            // elements from this one, except the one to be removed.
                            var newValues = new KeyValuePair<IAsyncLocal, object?>[_keyValues.Length - 1];
                            if (i != 0) Array.Copy(_keyValues, newValues, i);
                            if (i != _keyValues.Length - 1) Array.Copy(_keyValues, i + 1, newValues, i, _keyValues.Length - i - 1);
                            return new MultiElementAsyncLocalValueMap(newValues);
                        }
                    }
                }
 
                // The key does not already exist in this map.
 
                if (value is null && treatNullValueAsNonexistent)
                {
                    // We can simply return this same map, as there's nothing to add or remove.
                    return this;
                }
 
                // We need to create a new map that has the additional key/value pair.
                // If with the addition we can still fit in a multi map, create one.
                if (_keyValues.Length < MaxMultiElements)
                {
                    var newValues = new KeyValuePair<IAsyncLocal, object?>[_keyValues.Length + 1];
                    Array.Copy(_keyValues, newValues, _keyValues.Length);
                    newValues[_keyValues.Length] = KeyValuePair.Create(key, value);
                    return new MultiElementAsyncLocalValueMap(newValues);
                }
 
                // Otherwise, upgrade to a many map.
                var many = new ManyElementAsyncLocalValueMap(MaxMultiElements + 1);
                foreach (KeyValuePair<IAsyncLocal, object?> pair in _keyValues)
                {
                    many[pair.Key] = pair.Value;
                }
                many[key] = value;
                return many;
            }
 
            public bool TryGetValue(IAsyncLocal key, out object? value)
            {
                foreach (KeyValuePair<IAsyncLocal, object?> pair in _keyValues)
                {
                    if (ReferenceEquals(key, pair.Key))
                    {
                        value = pair.Value;
                        return true;
                    }
                }
                value = null;
                return false;
            }
        }
 
        /// <summary>Instance with any number of key/value pairs.</summary>
        private sealed class ManyElementAsyncLocalValueMap : Dictionary<IAsyncLocal, object?>, IAsyncLocalValueMap
        {
            public ManyElementAsyncLocalValueMap(int capacity) : base(capacity) { }
 
            public IAsyncLocalValueMap Set(IAsyncLocal key, object? value, bool treatNullValueAsNonexistent)
            {
                int count = Count;
                bool containsKey = ContainsKey(key);
 
                // If the value being set exists, create a new many map, copy all of the elements from this one,
                // and then store the new key/value pair into it.  This is the most common case.
                if (value is not null || !treatNullValueAsNonexistent)
                {
                    var map = new ManyElementAsyncLocalValueMap(count + (containsKey ? 0 : 1));
                    foreach (KeyValuePair<IAsyncLocal, object?> pair in this)
                    {
                        map[pair.Key] = pair.Value;
                    }
                    map[key] = value;
                    return map;
                }
 
                // Otherwise, the value is null and a null value may be treated as nonexistent. We can downgrade to a smaller
                // map rather than storing null.
 
                // If the key is contained in this map, we're going to create a new map that's one pair smaller.
                if (containsKey)
                {
                    // If the new count would be within range of a multi map instead of a many map,
                    // downgrade to the multi map, which uses less memory and is faster to access.
                    // Otherwise, just create a new many map that's missing this key.
                    if (count == MultiElementAsyncLocalValueMap.MaxMultiElements + 1)
                    {
                        var newValues = new KeyValuePair<IAsyncLocal, object?>[MultiElementAsyncLocalValueMap.MaxMultiElements];
                        int index = 0;
                        foreach (KeyValuePair<IAsyncLocal, object?> pair in this)
                        {
                            if (!ReferenceEquals(key, pair.Key))
                            {
                                newValues[index++] = pair;
                            }
                        }
                        Debug.Assert(index == MultiElementAsyncLocalValueMap.MaxMultiElements);
                        return new MultiElementAsyncLocalValueMap(newValues);
                    }
                    else
                    {
                        var map = new ManyElementAsyncLocalValueMap(count - 1);
                        foreach (KeyValuePair<IAsyncLocal, object?> pair in this)
                        {
                            if (!ReferenceEquals(key, pair.Key))
                            {
                                map[pair.Key] = pair.Value;
                            }
                        }
                        Debug.Assert(map.Count == count - 1);
                        return map;
                    }
                }
 
                // We were storing null and a null value may be treated as nonexistent, but the key wasn't in the map, so
                // there's nothing to change.  Just return this instance.
                return this;
            }
        }
    }
}