File: System\Dynamic\ExpandoObject.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;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Dynamic;
using System.Dynamic.Utils;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using AstUtils = System.Linq.Expressions.Utils;
 
namespace System.Dynamic
{
    /// <summary>
    /// Represents an object with members that can be dynamically added and removed at runtime.
    /// </summary>
    public sealed class ExpandoObject : IDynamicMetaObjectProvider, IDictionary<string, object?>, INotifyPropertyChanged
    {
        private static readonly MethodInfo s_expandoTryGetValue =
            typeof(RuntimeOps).GetMethod(nameof(RuntimeOps.ExpandoTryGetValue))!;
 
        private static readonly MethodInfo s_expandoTrySetValue =
            typeof(RuntimeOps).GetMethod(nameof(RuntimeOps.ExpandoTrySetValue))!;
 
        private static readonly MethodInfo s_expandoTryDeleteValue =
            typeof(RuntimeOps).GetMethod(nameof(RuntimeOps.ExpandoTryDeleteValue))!;
 
        private static readonly MethodInfo s_expandoPromoteClass =
            typeof(RuntimeOps).GetMethod(nameof(RuntimeOps.ExpandoPromoteClass))!;
 
        private static readonly MethodInfo s_expandoCheckVersion =
            typeof(RuntimeOps).GetMethod(nameof(RuntimeOps.ExpandoCheckVersion))!;
 
        internal readonly object LockObject;                          // the read-only field is used for locking the Expando object
        private ExpandoData _data;                                    // the data currently being held by the Expando object
        private int _count;                                           // the count of available members
 
        internal static readonly object Uninitialized = new object(); // A marker object used to identify that a value is uninitialized.
 
        internal const int AmbiguousMatchFound = -2;        // The value is used to indicate there exists ambiguous match in the Expando object
        internal const int NoMatch = -1;                    // The value is used to indicate there is no matching member
 
        private PropertyChangedEventHandler? _propertyChanged;
 
        /// <summary>
        /// Creates a new ExpandoObject with no members.
        /// </summary>
        public ExpandoObject()
        {
            _data = ExpandoData.Empty;
            LockObject = new object();
        }
 
        #region Get/Set/Delete Helpers
 
        /// <summary>
        /// Try to get the data stored for the specified class at the specified index.  If the
        /// class has changed a full lookup for the slot will be performed and the correct
        /// value will be retrieved.
        /// </summary>
        internal bool TryGetValue(object? indexClass, int index, string name, bool ignoreCase, out object? value)
        {
            // read the data now.  The data is immutable so we get a consistent view.
            // If there's a concurrent writer they will replace data and it just appears
            // that we won the race
            ExpandoData data = _data;
            if (data.Class != indexClass || ignoreCase)
            {
                /* Re-search for the index matching the name here if
                 *  1) the class has changed, we need to get the correct index and return
                 *  the value there.
                 *  2) the search is case insensitive:
                 *      a. the member specified by index may be deleted, but there might be other
                 *      members matching the name if the binder is case insensitive.
                 *      b. the member that exactly matches the name didn't exist before and exists now,
                 *      need to find the exact match.
                 */
                index = data.Class.GetValueIndex(name, ignoreCase, this);
                if (index == ExpandoObject.AmbiguousMatchFound)
                {
                    throw System.Linq.Expressions.Error.AmbiguousMatchInExpandoObject(name);
                }
            }
 
            if (index == ExpandoObject.NoMatch)
            {
                value = null;
                return false;
            }
 
            // Capture the value into a temp, so it doesn't get mutated after we check
            // for Uninitialized.
            object? temp = data[index];
            if (temp == Uninitialized)
            {
                value = null;
                return false;
            }
 
            // index is now known to be correct
            value = temp;
            return true;
        }
 
        /// <summary>
        /// Sets the data for the specified class at the specified index.  If the class has
        /// changed then a full look for the slot will be performed.  If the new class does
        /// not have the provided slot then the Expando's class will change. Only case sensitive
        /// setter is supported in ExpandoObject.
        /// </summary>
        internal void TrySetValue(object? indexClass, int index, object? value, string name, bool ignoreCase, bool add)
        {
            ExpandoData data;
            object? oldValue;
 
            lock (LockObject)
            {
                data = _data;
 
                if (data.Class != indexClass || ignoreCase)
                {
                    // The class has changed or we are doing a case-insensitive search,
                    // we need to get the correct index and set the value there.  If we
                    // don't have the value then we need to promote the class - that
                    // should only happen when we have multiple concurrent writers.
                    index = data.Class.GetValueIndex(name, ignoreCase, this);
                    if (index == ExpandoObject.AmbiguousMatchFound)
                    {
                        throw System.Linq.Expressions.Error.AmbiguousMatchInExpandoObject(name);
                    }
                    if (index == ExpandoObject.NoMatch)
                    {
                        // Before creating a new class with the new member, need to check
                        // if there is the exact same member but is deleted. We should reuse
                        // the class if there is such a member.
                        int exactMatch = ignoreCase ?
                            data.Class.GetValueIndexCaseSensitive(name) :
                            index;
                        if (exactMatch != ExpandoObject.NoMatch)
                        {
                            Debug.Assert(data[exactMatch] == Uninitialized);
                            index = exactMatch;
                        }
                        else
                        {
                            ExpandoClass newClass = data.Class.FindNewClass(name);
                            data = PromoteClassCore(data.Class, newClass);
                            // After the class promotion, there must be an exact match,
                            // so we can do case-sensitive search here.
                            index = data.Class.GetValueIndexCaseSensitive(name);
                            Debug.Assert(index != ExpandoObject.NoMatch);
                        }
                    }
                }
 
                // Setting an uninitialized member increases the count of available members
                oldValue = data[index];
                if (oldValue == Uninitialized)
                {
                    _count++;
                }
                else if (add)
                {
                    throw System.Linq.Expressions.Error.SameKeyExistsInExpando(name);
                }
 
                data[index] = value;
            }
 
            // Notify property changed outside the lock
            PropertyChangedEventHandler? propertyChanged = _propertyChanged;
            if (propertyChanged != null && value != oldValue)
            {
                propertyChanged(this, new PropertyChangedEventArgs(data.Class.Keys[index]));
            }
        }
 
        /// <summary>
        /// Deletes the data stored for the specified class at the specified index.
        /// </summary>
        internal bool TryDeleteValue(object? indexClass, int index, string name, bool ignoreCase, object? deleteValue)
        {
            ExpandoData data;
            lock (LockObject)
            {
                data = _data;
 
                if (data.Class != indexClass || ignoreCase)
                {
                    // the class has changed or we are doing a case-insensitive search,
                    // we need to get the correct index.  If there is no associated index
                    // we simply can't have the value and we return false.
                    index = data.Class.GetValueIndex(name, ignoreCase, this);
                    if (index == ExpandoObject.AmbiguousMatchFound)
                    {
                        throw System.Linq.Expressions.Error.AmbiguousMatchInExpandoObject(name);
                    }
                }
                if (index == ExpandoObject.NoMatch)
                {
                    return false;
                }
 
                object? oldValue = data[index];
                if (oldValue == Uninitialized)
                {
                    return false;
                }
 
                // Make sure the value matches, if requested.
                //
                // It's a shame we have to call Equals with the lock held but
                // there doesn't seem to be a good way around that, and
                // ConcurrentDictionary in mscorlib does the same thing.
                if (deleteValue != Uninitialized && !object.Equals(oldValue, deleteValue))
                {
                    return false;
                }
 
                data[index] = Uninitialized;
 
                // Deleting an available member decreases the count of available members
                _count--;
            }
 
            // Notify property changed outside the lock
            _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(data.Class.Keys[index]));
 
            return true;
        }
 
        /// <summary>
        /// Returns true if the member at the specified index has been deleted,
        /// otherwise false. Call this function holding the lock.
        /// </summary>
        internal bool IsDeletedMember(int index)
        {
            ContractUtils.AssertLockHeld(LockObject);
            Debug.Assert(index >= 0 && index <= _data.Length);
 
            if (index == _data.Length)
            {
                // The member is a newly added by SetMemberBinder and not in data yet
                return false;
            }
 
            return _data[index] == ExpandoObject.Uninitialized;
        }
 
        /// <summary>
        /// Exposes the ExpandoClass which we've associated with this
        /// Expando object.  Used for type checks in rules.
        /// </summary>
        internal ExpandoClass Class => _data.Class;
 
        /// <summary>
        /// Promotes the class from the old type to the new type and returns the new
        /// ExpandoData object.
        /// </summary>
        private ExpandoData PromoteClassCore(ExpandoClass oldClass, ExpandoClass newClass)
        {
            Debug.Assert(oldClass != newClass);
            ContractUtils.AssertLockHeld(LockObject);
 
            if (_data.Class == oldClass)
            {
                _data = _data.UpdateClass(newClass);
            }
 
            return _data;
        }
 
        /// <summary>
        /// Internal helper to promote a class.  Called from our RuntimeOps helper.  This
        /// version simply doesn't expose the ExpandoData object which is a private
        /// data structure.
        /// </summary>
        internal void PromoteClass(object oldClass, object newClass)
        {
            lock (LockObject)
            {
                PromoteClassCore((ExpandoClass)oldClass, (ExpandoClass)newClass);
            }
        }
 
        #endregion
 
        #region IDynamicMetaObjectProvider Members
 
        DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(Expression parameter)
        {
            return new MetaExpando(parameter, this);
        }
 
        #endregion
 
        #region Helper methods
        private void TryAddMember(string key, object? value)
        {
            ArgumentNullException.ThrowIfNull(key);
            // Pass null to the class, which forces lookup.
            TrySetValue(null, -1, value, key, ignoreCase: false, add: true);
        }
 
        private bool TryGetValueForKey(string key, out object? value)
        {
            // Pass null to the class, which forces lookup.
            return TryGetValue(null, -1, key, ignoreCase: false, value: out value);
        }
 
        private bool ExpandoContainsKey(string key)
        {
            ContractUtils.AssertLockHeld(LockObject);
            return _data.Class.GetValueIndexCaseSensitive(key) >= 0;
        }
 
        // We create a non-generic type for the debug view for each different collection type
        // that uses DebuggerTypeProxy, instead of defining a generic debug view type and
        // using different instantiations. The reason for this is that support for generics
        // with using DebuggerTypeProxy is limited. For C#, DebuggerTypeProxy supports only
        // open types (from MSDN https://learn.microsoft.com/visualstudio/debugger/using-debuggertypeproxy-attribute).
        private sealed class KeyCollectionDebugView
        {
            private readonly ICollection<string> _collection;
 
            public KeyCollectionDebugView(ICollection<string> collection)
            {
                ArgumentNullException.ThrowIfNull(collection);
                _collection = collection;
            }
 
            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
            public string[] Items
            {
                get
                {
                    string[] items = new string[_collection.Count];
                    _collection.CopyTo(items, 0);
                    return items;
                }
            }
        }
 
        [DebuggerTypeProxy(typeof(KeyCollectionDebugView))]
        [DebuggerDisplay("Count = {Count}")]
        private sealed class KeyCollection : ICollection<string>
        {
            private readonly ExpandoObject _expando;
            private readonly int _expandoVersion;
            private readonly int _expandoCount;
            private readonly ExpandoData _expandoData;
 
            internal KeyCollection(ExpandoObject expando)
            {
                lock (expando.LockObject)
                {
                    _expando = expando;
                    _expandoVersion = expando._data.Version;
                    _expandoCount = expando._count;
                    _expandoData = expando._data;
                }
            }
 
            private void CheckVersion()
            {
                if (_expando._data.Version != _expandoVersion || _expandoData != _expando._data)
                {
                    //the underlying expando object has changed
                    throw System.Linq.Expressions.Error.CollectionModifiedWhileEnumerating();
                }
            }
 
            #region ICollection<string> Members
 
            public void Add(string item)
            {
                throw System.Linq.Expressions.Error.CollectionReadOnly();
            }
 
            public void Clear()
            {
                throw System.Linq.Expressions.Error.CollectionReadOnly();
            }
 
            public bool Contains(string item)
            {
                lock (_expando.LockObject)
                {
                    CheckVersion();
                    return _expando.ExpandoContainsKey(item);
                }
            }
 
            public void CopyTo(string[] array, int arrayIndex)
            {
                ArgumentNullException.ThrowIfNull(array);
                ContractUtils.RequiresArrayRange(array, arrayIndex, _expandoCount, nameof(arrayIndex), nameof(Count));
                lock (_expando.LockObject)
                {
                    CheckVersion();
                    ExpandoData data = _expando._data;
                    for (int i = 0; i < data.Class.Keys.Length; i++)
                    {
                        if (data[i] != Uninitialized)
                        {
                            array[arrayIndex++] = data.Class.Keys[i];
                        }
                    }
                }
            }
 
            public int Count
            {
                get
                {
                    CheckVersion();
                    return _expandoCount;
                }
            }
 
            public bool IsReadOnly => true;
 
            public bool Remove(string item)
            {
                throw System.Linq.Expressions.Error.CollectionReadOnly();
            }
 
            #endregion
 
            #region IEnumerable<string> Members
 
            public IEnumerator<string> GetEnumerator()
            {
                for (int i = 0, n = _expandoData.Class.Keys.Length; i < n; i++)
                {
                    CheckVersion();
                    if (_expandoData[i] != Uninitialized)
                    {
                        yield return _expandoData.Class.Keys[i];
                    }
                }
            }
 
            #endregion
 
            #region IEnumerable Members
 
            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }
 
            #endregion
        }
 
        // We create a non-generic type for the debug view for each different collection type
        // that uses DebuggerTypeProxy, instead of defining a generic debug view type and
        // using different instantiations. The reason for this is that support for generics
        // with using DebuggerTypeProxy is limited. For C#, DebuggerTypeProxy supports only
        // open types (from MSDN https://learn.microsoft.com/visualstudio/debugger/using-debuggertypeproxy-attribute).
        private sealed class ValueCollectionDebugView
        {
            private readonly ICollection<object> _collection;
 
            public ValueCollectionDebugView(ICollection<object> collection)
            {
                ArgumentNullException.ThrowIfNull(collection);
                _collection = collection;
            }
 
            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
            public object[] Items
            {
                get
                {
                    object[] items = new object[_collection.Count];
                    _collection.CopyTo(items, 0);
                    return items;
                }
            }
        }
 
        [DebuggerTypeProxy(typeof(ValueCollectionDebugView))]
        [DebuggerDisplay("Count = {Count}")]
        private sealed class ValueCollection : ICollection<object?>
        {
            private readonly ExpandoObject _expando;
            private readonly int _expandoVersion;
            private readonly int _expandoCount;
            private readonly ExpandoData _expandoData;
 
            internal ValueCollection(ExpandoObject expando)
            {
                lock (expando.LockObject)
                {
                    _expando = expando;
                    _expandoVersion = expando._data.Version;
                    _expandoCount = expando._count;
                    _expandoData = expando._data;
                }
            }
 
            private void CheckVersion()
            {
                if (_expando._data.Version != _expandoVersion || _expandoData != _expando._data)
                {
                    //the underlying expando object has changed
                    throw System.Linq.Expressions.Error.CollectionModifiedWhileEnumerating();
                }
            }
 
            #region ICollection<string> Members
 
            public void Add(object? item)
            {
                throw System.Linq.Expressions.Error.CollectionReadOnly();
            }
 
            public void Clear()
            {
                throw System.Linq.Expressions.Error.CollectionReadOnly();
            }
 
            public bool Contains(object? item)
            {
                lock (_expando.LockObject)
                {
                    CheckVersion();
 
                    ExpandoData data = _expando._data;
                    for (int i = 0; i < data.Class.Keys.Length; i++)
                    {
                        // See comment in TryDeleteValue; it's okay to call
                        // object.Equals with the lock held.
                        if (object.Equals(data[i], item))
                        {
                            return true;
                        }
                    }
                    return false;
                }
            }
 
            public void CopyTo(object?[] array, int arrayIndex)
            {
                ArgumentNullException.ThrowIfNull(array);
                ContractUtils.RequiresArrayRange(array, arrayIndex, _expandoCount, nameof(arrayIndex), nameof(Count));
                lock (_expando.LockObject)
                {
                    CheckVersion();
                    ExpandoData data = _expando._data;
                    for (int i = 0; i < data.Class.Keys.Length; i++)
                    {
                        if (data[i] != Uninitialized)
                        {
                            array[arrayIndex++] = data[i];
                        }
                    }
                }
            }
 
            public int Count
            {
                get
                {
                    CheckVersion();
                    return _expandoCount;
                }
            }
 
            public bool IsReadOnly => true;
 
            public bool Remove(object? item)
            {
                throw System.Linq.Expressions.Error.CollectionReadOnly();
            }
 
            #endregion
 
            #region IEnumerable<string> Members
 
            public IEnumerator<object?> GetEnumerator()
            {
                ExpandoData data = _expando._data;
                for (int i = 0; i < data.Class.Keys.Length; i++)
                {
                    CheckVersion();
                    // Capture the value into a temp so we don't inadvertently
                    // return Uninitialized.
                    object? temp = data[i];
                    if (temp != Uninitialized)
                    {
                        yield return temp;
                    }
                }
            }
 
            #endregion
 
            #region IEnumerable Members
 
            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }
 
            #endregion
        }
 
        #endregion
 
        #region IDictionary<string, object> Members
 
        ICollection<string> IDictionary<string, object?>.Keys => new KeyCollection(this);
 
        ICollection<object?> IDictionary<string, object?>.Values => new ValueCollection(this);
 
        object? IDictionary<string, object?>.this[string key]
        {
            get
            {
                if (!TryGetValueForKey(key, out object? value))
                {
                    throw System.Linq.Expressions.Error.KeyDoesNotExistInExpando(key);
                }
                return value;
            }
            set
            {
                ArgumentNullException.ThrowIfNull(key);
                // Pass null to the class, which forces lookup.
                TrySetValue(null, -1, value, key, ignoreCase: false, add: false);
            }
        }
 
        void IDictionary<string, object?>.Add(string key, object? value)
        {
            this.TryAddMember(key, value);
        }
 
        bool IDictionary<string, object?>.ContainsKey(string key)
        {
            ArgumentNullException.ThrowIfNull(key);
 
            ExpandoData data = _data;
            int index = data.Class.GetValueIndexCaseSensitive(key);
            return index >= 0 && data[index] != Uninitialized;
        }
 
        bool IDictionary<string, object?>.Remove(string key)
        {
            ArgumentNullException.ThrowIfNull(key);
            // Pass null to the class, which forces lookup.
            return TryDeleteValue(null, -1, key, ignoreCase: false, deleteValue: Uninitialized);
        }
 
        bool IDictionary<string, object?>.TryGetValue(string key, out object? value)
        {
            return TryGetValueForKey(key, out value);
        }
 
        #endregion
 
        #region ICollection<KeyValuePair<string, object>> Members
 
        int ICollection<KeyValuePair<string, object?>>.Count => _count;
 
        bool ICollection<KeyValuePair<string, object?>>.IsReadOnly => false;
 
        void ICollection<KeyValuePair<string, object?>>.Add(KeyValuePair<string, object?> item)
        {
            TryAddMember(item.Key, item.Value);
        }
 
        void ICollection<KeyValuePair<string, object?>>.Clear()
        {
            // We remove both class and data!
            ExpandoData data;
            lock (LockObject)
            {
                data = _data;
                _data = ExpandoData.Empty;
                _count = 0;
            }
 
            // Notify property changed for all properties.
            var propertyChanged = _propertyChanged;
            if (propertyChanged != null)
            {
                for (int i = 0, n = data.Class.Keys.Length; i < n; i++)
                {
                    if (data[i] != Uninitialized)
                    {
                        propertyChanged(this, new PropertyChangedEventArgs(data.Class.Keys[i]));
                    }
                }
            }
        }
 
        bool ICollection<KeyValuePair<string, object?>>.Contains(KeyValuePair<string, object?> item)
        {
            if (!TryGetValueForKey(item.Key, out object? value))
            {
                return false;
            }
 
            return object.Equals(value, item.Value);
        }
 
        void ICollection<KeyValuePair<string, object?>>.CopyTo(KeyValuePair<string, object?>[] array, int arrayIndex)
        {
            ArgumentNullException.ThrowIfNull(array);
 
            // We want this to be atomic and not throw, though we must do the range checks inside this lock.
            lock (LockObject)
            {
                ContractUtils.RequiresArrayRange(array, arrayIndex, _count, nameof(arrayIndex), nameof(ICollection<KeyValuePair<string, object>>.Count));
                foreach (KeyValuePair<string, object?> item in this)
                {
                    array[arrayIndex++] = item;
                }
            }
        }
 
        bool ICollection<KeyValuePair<string, object?>>.Remove(KeyValuePair<string, object?> item)
        {
            return TryDeleteValue(null, -1, item.Key, ignoreCase: false, deleteValue: item.Value);
        }
 
        #endregion
 
        #region IEnumerable<KeyValuePair<string, object>> Member
 
        IEnumerator<KeyValuePair<string, object?>> IEnumerable<KeyValuePair<string, object?>>.GetEnumerator()
        {
            ExpandoData data = _data;
            return GetExpandoEnumerator(data, data.Version);
        }
 
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            ExpandoData data = _data;
            return GetExpandoEnumerator(data, data.Version);
        }
 
        // Note: takes the data and version as parameters so they will be
        // captured before the first call to MoveNext().
        private IEnumerator<KeyValuePair<string, object?>> GetExpandoEnumerator(ExpandoData data, int version)
        {
            for (int i = 0; i < data.Class.Keys.Length; i++)
            {
                if (_data.Version != version || data != _data)
                {
                    // The underlying expando object has changed:
                    // 1) the version of the expando data changed
                    // 2) the data object is changed
                    throw System.Linq.Expressions.Error.CollectionModifiedWhileEnumerating();
                }
                // Capture the value into a temp so we don't inadvertently
                // return Uninitialized.
                object? temp = data[i];
                if (temp != Uninitialized)
                {
                    yield return new KeyValuePair<string, object?>(data.Class.Keys[i], temp);
                }
            }
        }
 
        #endregion
 
        #region MetaExpando
 
        private sealed class MetaExpando : DynamicMetaObject
        {
            public MetaExpando(Expression expression, ExpandoObject value)
                : base(expression, BindingRestrictions.Empty, value)
            {
            }
 
            private DynamicMetaObject BindGetOrInvokeMember(DynamicMetaObjectBinder binder, string name, bool ignoreCase, DynamicMetaObject fallback, Func<DynamicMetaObject, DynamicMetaObject>? fallbackInvoke)
            {
                ExpandoClass klass = Value.Class;
 
                //try to find the member, including the deleted members
                int index = klass.GetValueIndex(name, ignoreCase, Value);
 
                ParameterExpression value = Expression.Parameter(typeof(object), "value");
 
                Expression tryGetValue = Expression.Call(
                    s_expandoTryGetValue,
                    GetLimitedSelf(),
                    Expression.Constant(klass, typeof(object)),
                    AstUtils.Constant(index),
                    Expression.Constant(name),
                    AstUtils.Constant(ignoreCase),
                    value
                );
 
                var result = new DynamicMetaObject(value, BindingRestrictions.Empty);
                if (fallbackInvoke != null)
                {
                    result = fallbackInvoke(result);
                }
 
                result = new DynamicMetaObject(
                    Expression.Block(
                        new TrueReadOnlyCollection<ParameterExpression>(value),
                        new TrueReadOnlyCollection<Expression>(
                            Expression.Condition(
                                tryGetValue,
                                result.Expression,
                                fallback.Expression,
                                typeof(object)
                            )
                        )
                    ),
                    result.Restrictions.Merge(fallback.Restrictions)
                );
 
                return AddDynamicTestAndDefer(binder, Value.Class, null, result);
            }
 
            public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
            {
                ArgumentNullException.ThrowIfNull(binder);
                return BindGetOrInvokeMember(
                    binder,
                    binder.Name,
                    binder.IgnoreCase,
                    binder.FallbackGetMember(this),
                    null
                );
            }
 
            public override DynamicMetaObject BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args)
            {
                ArgumentNullException.ThrowIfNull(binder);
                return BindGetOrInvokeMember(
                    binder,
                    binder.Name,
                    binder.IgnoreCase,
                    binder.FallbackInvokeMember(this, args),
                    value => binder.FallbackInvoke(value, args, null)
                );
            }
 
            public override DynamicMetaObject BindSetMember(SetMemberBinder binder, DynamicMetaObject value)
            {
                ArgumentNullException.ThrowIfNull(binder);
                ArgumentNullException.ThrowIfNull(value);
 
                ExpandoClass klass;
                int index;
 
                ExpandoClass? originalClass = GetClassEnsureIndex(binder.Name, binder.IgnoreCase, Value, out klass, out index);
 
                return AddDynamicTestAndDefer(
                    binder,
                    klass,
                    originalClass,
                    new DynamicMetaObject(
                        Expression.Call(
                            s_expandoTrySetValue,
                            GetLimitedSelf(),
                            Expression.Constant(klass, typeof(object)),
                            AstUtils.Constant(index),
                            Expression.Convert(value.Expression, typeof(object)),
                            Expression.Constant(binder.Name),
                            AstUtils.Constant(binder.IgnoreCase)
                        ),
                        BindingRestrictions.Empty
                    )
                );
            }
 
            public override DynamicMetaObject BindDeleteMember(DeleteMemberBinder binder)
            {
                ArgumentNullException.ThrowIfNull(binder);
 
                int index = Value.Class.GetValueIndex(binder.Name, binder.IgnoreCase, Value);
 
                Expression tryDelete = Expression.Call(
                    s_expandoTryDeleteValue,
                    GetLimitedSelf(),
                    Expression.Constant(Value.Class, typeof(object)),
                    AstUtils.Constant(index),
                    Expression.Constant(binder.Name),
                    AstUtils.Constant(binder.IgnoreCase)
                );
                DynamicMetaObject fallback = binder.FallbackDeleteMember(this);
 
                DynamicMetaObject target = new DynamicMetaObject(
                    Expression.IfThen(Expression.Not(tryDelete), fallback.Expression),
                    fallback.Restrictions
                );
 
                return AddDynamicTestAndDefer(binder, Value.Class, null, target);
            }
 
            public override IEnumerable<string> GetDynamicMemberNames()
            {
                var expandoData = Value._data;
                var klass = expandoData.Class;
                for (int i = 0; i < klass.Keys.Length; i++)
                {
                    object? val = expandoData[i];
                    if (val != ExpandoObject.Uninitialized)
                    {
                        yield return klass.Keys[i];
                    }
                }
            }
 
            /// <summary>
            /// Adds a dynamic test which checks if the version has changed.  The test is only necessary for
            /// performance as the methods will do the correct thing if called with an incorrect version.
            /// </summary>
            private DynamicMetaObject AddDynamicTestAndDefer(DynamicMetaObjectBinder binder, ExpandoClass klass, ExpandoClass? originalClass, DynamicMetaObject succeeds)
            {
                Expression ifTestSucceeds = succeeds.Expression;
                if (originalClass != null)
                {
                    // we are accessing a member which has not yet been defined on this class.
                    // We force a class promotion after the type check.  If the class changes the
                    // promotion will fail and the set/delete will do a full lookup using the new
                    // class to discover the name.
                    Debug.Assert(originalClass != klass);
 
                    ifTestSucceeds = Expression.Block(
                        Expression.Call(
                            null,
                            s_expandoPromoteClass,
                            GetLimitedSelf(),
                            Expression.Constant(originalClass, typeof(object)),
                            Expression.Constant(klass, typeof(object))
                        ),
                        succeeds.Expression
                    );
                }
 
                return new DynamicMetaObject(
                    Expression.Condition(
                        Expression.Call(
                            null,
                            s_expandoCheckVersion,
                            GetLimitedSelf(),
                            Expression.Constant(originalClass ?? klass, typeof(object))
                        ),
                        ifTestSucceeds,
                        binder.GetUpdateExpression(ifTestSucceeds.Type)
                    ),
                    GetRestrictions().Merge(succeeds.Restrictions)
                );
            }
 
            /// <summary>
            /// Gets the class and the index associated with the given name.  Does not update the expando object.  Instead
            /// this returns both the original and desired new class.  A rule is created which includes the test for the
            /// original class, the promotion to the new class, and the set/delete based on the class post-promotion.
            /// </summary>
            private ExpandoClass? GetClassEnsureIndex(string name, bool caseInsensitive, ExpandoObject obj, out ExpandoClass klass, out int index)
            {
                ExpandoClass originalClass = Value.Class;
 
                index = originalClass.GetValueIndex(name, caseInsensitive, obj);
                if (index == ExpandoObject.AmbiguousMatchFound)
                {
                    klass = originalClass;
                    return null;
                }
                if (index == ExpandoObject.NoMatch)
                {
                    // go ahead and find a new class now...
                    ExpandoClass newClass = originalClass.FindNewClass(name);
 
                    klass = newClass;
                    index = newClass.GetValueIndexCaseSensitive(name);
 
                    Debug.Assert(index != ExpandoObject.NoMatch);
                    return originalClass;
                }
                else
                {
                    klass = originalClass;
                    return null;
                }
            }
 
            /// <summary>
            /// Returns our Expression converted to our known LimitType
            /// </summary>
            private Expression GetLimitedSelf()
            {
                if (TypeUtils.AreEquivalent(Expression.Type, LimitType))
                {
                    return Expression;
                }
                return Expression.Convert(Expression, LimitType);
            }
 
            /// <summary>
            /// Returns a Restrictions object which includes our current restrictions merged
            /// with a restriction limiting our type
            /// </summary>
            private BindingRestrictions GetRestrictions()
            {
                Debug.Assert(Restrictions == BindingRestrictions.Empty, "We don't merge, restrictions are always empty");
 
                return BindingRestrictions.GetTypeRestriction(this);
            }
 
            public new ExpandoObject Value => (ExpandoObject)base.Value!;
        }
 
        #endregion
 
        #region ExpandoData
 
        /// <summary>
        /// Stores the class and the data associated with the class as one atomic
        /// pair.  This enables us to do a class check in a thread safe manner w/o
        /// requiring locks.
        /// </summary>
        private sealed class ExpandoData
        {
            internal static readonly ExpandoData Empty = new ExpandoData();
 
            /// <summary>
            /// the dynamically assigned class associated with the Expando object
            /// </summary>
            internal readonly ExpandoClass Class;
 
            /// <summary>
            /// data stored in the expando object, key names are stored in the class.
            ///
            /// Expando._data must be locked when mutating the value.  Otherwise a copy of it
            /// could be made and lose values.
            /// </summary>
            private readonly object?[] _dataArray;
 
            /// <summary>
            /// Indexer for getting/setting the data
            /// </summary>
            internal object? this[int index]
            {
                get
                {
                    return _dataArray[index];
                }
                set
                {
                    //when the array is updated, version increases, even the new value is the same
                    //as previous. Dictionary type has the same behavior.
                    _version++;
                    _dataArray[index] = value;
                }
            }
 
            internal int Version => _version;
 
            internal int Length => _dataArray.Length;
 
            /// <summary>
            /// Constructs an empty ExpandoData object with the empty class and no data.
            /// </summary>
            private ExpandoData()
            {
                Class = ExpandoClass.Empty;
                _dataArray = Array.Empty<object>();
            }
 
            /// <summary>
            /// the version of the ExpandoObject that tracks set and delete operations
            /// </summary>
            private int _version;
 
            /// <summary>
            /// Constructs a new ExpandoData object with the specified class and data.
            /// </summary>
            internal ExpandoData(ExpandoClass klass, object?[] data, int version)
            {
                Class = klass;
                _dataArray = data;
                _version = version;
            }
 
            /// <summary>
            /// Update the associated class and increases the storage for the data array if needed.
            /// </summary>
            internal ExpandoData UpdateClass(ExpandoClass newClass)
            {
                if (_dataArray.Length >= newClass.Keys.Length)
                {
                    // we have extra space in our buffer, just initialize it to Uninitialized.
                    this[newClass.Keys.Length - 1] = ExpandoObject.Uninitialized;
                    return new ExpandoData(newClass, _dataArray, _version);
                }
                else
                {
                    // we've grown too much - we need a new object array
                    int oldLength = _dataArray.Length;
                    object[] arr = new object[GetAlignedSize(newClass.Keys.Length)];
                    Array.Copy(_dataArray, arr, _dataArray.Length);
                    ExpandoData newData = new ExpandoData(newClass, arr, _version);
                    newData[oldLength] = ExpandoObject.Uninitialized;
                    return newData;
                }
            }
 
            private static int GetAlignedSize(int len)
            {
                // the alignment of the array for storage of values (must be a power of two)
                const int DataArrayAlignment = 8;
 
                // round up and then mask off lower bits
                return (len + (DataArrayAlignment - 1)) & (~(DataArrayAlignment - 1));
            }
        }
 
        #endregion
 
        #region INotifyPropertyChanged
 
        event PropertyChangedEventHandler? INotifyPropertyChanged.PropertyChanged
        {
            add { _propertyChanged += value; }
            remove { _propertyChanged -= value; }
        }
 
        #endregion
    }
}
 
namespace System.Runtime.CompilerServices
{
    //
    // Note: these helpers are kept as simple wrappers so they have a better
    // chance of being inlined.
    //
    public static partial class RuntimeOps
    {
        /// <summary>
        /// Gets the value of an item in an expando object.
        /// </summary>
        /// <param name="expando">The expando object.</param>
        /// <param name="indexClass">The class of the expando object.</param>
        /// <param name="index">The index of the member.</param>
        /// <param name="name">The name of the member.</param>
        /// <param name="ignoreCase">true if the name should be matched ignoring case; false otherwise.</param>
        /// <param name="value">The out parameter containing the value of the member.</param>
        /// <returns>True if the member exists in the expando object, otherwise false.</returns>
        [Obsolete("RuntimeOps has been deprecated and is not supported.", error: true), EditorBrowsable(EditorBrowsableState.Never)]
        public static bool ExpandoTryGetValue(ExpandoObject expando, object? indexClass, int index, string name, bool ignoreCase, out object? value)
        {
            return expando.TryGetValue(indexClass, index, name, ignoreCase, out value);
        }
 
        /// <summary>
        /// Sets the value of an item in an expando object.
        /// </summary>
        /// <param name="expando">The expando object.</param>
        /// <param name="indexClass">The class of the expando object.</param>
        /// <param name="index">The index of the member.</param>
        /// <param name="value">The value of the member.</param>
        /// <param name="name">The name of the member.</param>
        /// <param name="ignoreCase">true if the name should be matched ignoring case; false otherwise.</param>
        /// <returns>
        /// Returns the index for the set member.
        /// </returns>
        [Obsolete("RuntimeOps has been deprecated and is not supported.", error: true), EditorBrowsable(EditorBrowsableState.Never)]
        public static object? ExpandoTrySetValue(ExpandoObject expando, object? indexClass, int index, object? value, string name, bool ignoreCase)
        {
            expando.TrySetValue(indexClass, index, value, name, ignoreCase, false);
            return value;
        }
 
        /// <summary>
        /// Deletes the value of an item in an expando object.
        /// </summary>
        /// <param name="expando">The expando object.</param>
        /// <param name="indexClass">The class of the expando object.</param>
        /// <param name="index">The index of the member.</param>
        /// <param name="name">The name of the member.</param>
        /// <param name="ignoreCase">true if the name should be matched ignoring case; false otherwise.</param>
        /// <returns>true if the item was successfully removed; otherwise, false.</returns>
        [Obsolete("RuntimeOps has been deprecated and is not supported.", error: true), EditorBrowsable(EditorBrowsableState.Never)]
        public static bool ExpandoTryDeleteValue(ExpandoObject expando, object? indexClass, int index, string name, bool ignoreCase)
        {
            return expando.TryDeleteValue(indexClass, index, name, ignoreCase, ExpandoObject.Uninitialized);
        }
 
        /// <summary>
        /// Checks the version of the expando object.
        /// </summary>
        /// <param name="expando">The expando object.</param>
        /// <param name="version">The version to check.</param>
        /// <returns>true if the version is equal; otherwise, false.</returns>
        [Obsolete("RuntimeOps has been deprecated and is not supported.", error: true), EditorBrowsable(EditorBrowsableState.Never)]
        public static bool ExpandoCheckVersion(ExpandoObject expando, object? version)
        {
            return expando.Class == version;
        }
 
        /// <summary>
        /// Promotes an expando object from one class to a new class.
        /// </summary>
        /// <param name="expando">The expando object.</param>
        /// <param name="oldClass">The old class of the expando object.</param>
        /// <param name="newClass">The new class of the expando object.</param>
        [Obsolete("RuntimeOps has been deprecated and is not supported.", error: true), EditorBrowsable(EditorBrowsableState.Never)]
        public static void ExpandoPromoteClass(ExpandoObject expando, object oldClass, object newClass)
        {
            expando.PromoteClass(oldClass, newClass);
        }
    }
}