File: System\Dynamic\ExpandoClass.cs
Web Access
Project: src\src\libraries\System.Linq.Expressions\src\System.Linq.Expressions.csproj (System.Linq.Expressions)
// 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;
 
namespace System.Dynamic
{
    /// <summary>
    /// Represents a dynamically assigned class.  Expando objects which share the same
    /// members will share the same class.  Classes are dynamically assigned as the
    /// expando object gains members.
    /// </summary>
    internal sealed class ExpandoClass
    {
        private readonly string[] _keys;                            // list of names associated with each element in the data array, sorted
        private readonly int _hashCode;                             // pre-calculated hash code of all the keys the class contains
        private Dictionary<int, List<WeakReference>>? _transitions;  // cached transitions
 
        private const int EmptyHashCode = 6551;                     // hash code of the empty ExpandoClass.
 
        internal static readonly ExpandoClass Empty = new ExpandoClass();    // The empty Expando class - all Expando objects start off w/ this class.
 
        /// <summary>
        /// Constructs the empty ExpandoClass.  This is the class used when an
        /// empty Expando object is initially constructed.
        /// </summary>
        internal ExpandoClass()
        {
            _hashCode = EmptyHashCode;
            _keys = Array.Empty<string>();
        }
 
        /// <summary>
        /// Constructs a new ExpandoClass that can hold onto the specified keys.  The
        /// keys must be sorted ordinally.  The hash code must be precalculated for
        /// the keys.
        /// </summary>
        internal ExpandoClass(string[] keys, int hashCode)
        {
            _hashCode = hashCode;
            _keys = keys;
        }
 
        /// <summary>
        /// Finds or creates a new ExpandoClass given the existing set of keys
        /// in this ExpandoClass plus the new key to be added. Members in an
        /// ExpandoClass are always stored case sensitively.
        /// </summary>
        internal ExpandoClass FindNewClass(string newKey)
        {
            // just XOR the newKey hash code
            int hashCode = _hashCode ^ newKey.GetHashCode();
 
            lock (this)
            {
                List<WeakReference> infos = GetTransitionList(hashCode);
 
                for (int i = 0; i < infos.Count; i++)
                {
                    ExpandoClass? klass = infos[i].Target as ExpandoClass;
                    if (klass == null)
                    {
                        infos.RemoveAt(i);
                        i--;
                        continue;
                    }
 
                    if (string.Equals(klass._keys[klass._keys.Length - 1], newKey, StringComparison.Ordinal))
                    {
                        // the new key is the key we added in this transition
                        return klass;
                    }
                }
 
                // no applicable transition, create a new one
                string[] keys = new string[_keys.Length + 1];
                Array.Copy(_keys, keys, _keys.Length);
                keys[_keys.Length] = newKey;
                ExpandoClass ec = new ExpandoClass(keys, hashCode);
 
                infos.Add(new WeakReference(ec));
                return ec;
            }
        }
 
        /// <summary>
        /// Gets the lists of transitions that are valid from this ExpandoClass
        /// to an ExpandoClass whose keys hash to the appropriate hash code.
        /// </summary>
        private List<WeakReference> GetTransitionList(int hashCode)
        {
            _transitions ??= new Dictionary<int, List<WeakReference>>();
 
            if (!_transitions.TryGetValue(hashCode, out List<WeakReference>? infos))
            {
                _transitions[hashCode] = infos = new List<WeakReference>();
            }
 
            return infos;
        }
 
        /// <summary>
        /// Gets the index at which the value should be stored for the specified name.
        /// </summary>
        internal int GetValueIndex(string name, bool caseInsensitive, ExpandoObject obj)
        {
            if (caseInsensitive)
            {
                return GetValueIndexCaseInsensitive(name, obj);
            }
            else
            {
                return GetValueIndexCaseSensitive(name);
            }
        }
 
        /// <summary>
        /// Gets the index at which the value should be stored for the specified name
        /// case sensitively. Returns the index even if the member is marked as deleted.
        /// </summary>
        internal int GetValueIndexCaseSensitive(string name)
        {
            for (int i = 0; i < _keys.Length; i++)
            {
                if (string.Equals(
                    _keys[i],
                    name,
                    StringComparison.Ordinal))
                {
                    return i;
                }
            }
            return ExpandoObject.NoMatch;
        }
 
        /// <summary>
        /// Gets the index at which the value should be stored for the specified name,
        /// the method is only used in the case-insensitive case.
        /// </summary>
        /// <param name="name">the name of the member</param>
        /// <param name="obj">The ExpandoObject associated with the class
        /// that is used to check if a member has been deleted.</param>
        /// <returns>
        /// the exact match if there is one
        /// if there is exactly one member with case insensitive match, return it
        /// otherwise we throw AmbiguousMatchException.
        /// </returns>
        private int GetValueIndexCaseInsensitive(string name, ExpandoObject obj)
        {
            int caseInsensitiveMatch = ExpandoObject.NoMatch; //the location of the case-insensitive matching member
            lock (obj.LockObject)
            {
                for (int i = _keys.Length - 1; i >= 0; i--)
                {
                    if (string.Equals(
                        _keys[i],
                        name,
                        StringComparison.OrdinalIgnoreCase))
                    {
                        //if the matching member is deleted, continue searching
                        if (!obj.IsDeletedMember(i))
                        {
                            if (caseInsensitiveMatch == ExpandoObject.NoMatch)
                            {
                                caseInsensitiveMatch = i;
                            }
                            else
                            {
                                //Ambiguous match, stop searching
                                return ExpandoObject.AmbiguousMatchFound;
                            }
                        }
                    }
                }
            }
            //There is exactly one member with case insensitive match.
            return caseInsensitiveMatch;
        }
 
        /// <summary>
        /// Gets the names of the keys that can be stored in the Expando class.  The
        /// list is sorted ordinally.
        /// </summary>
        internal string[] Keys => _keys;
    }
}