1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
4 
5 using System.Collections.Generic;
6 using System.ComponentModel;
7 using System.Diagnostics;
8 using System.Dynamic;
9 using System.Dynamic.Utils;
10 using System.Linq.Expressions;
11 using System.Reflection;
12 using System.Runtime.CompilerServices;
13 using AstUtils = System.Linq.Expressions.Utils;
14 
15 namespace System.Dynamic
16 {
17     /// <summary>
18     /// Represents an object with members that can be dynamically added and removed at runtime.
19     /// </summary>
20     [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
21     public sealed class ExpandoObject : IDynamicMetaObjectProvider, IDictionary<string, object>, INotifyPropertyChanged
22     {
23         private static readonly MethodInfo s_expandoTryGetValue =
24             typeof(RuntimeOps).GetMethod(nameof(RuntimeOps.ExpandoTryGetValue));
25 
26         private static readonly MethodInfo s_expandoTrySetValue =
27             typeof(RuntimeOps).GetMethod(nameof(RuntimeOps.ExpandoTrySetValue));
28 
29         private static readonly MethodInfo s_expandoTryDeleteValue =
30             typeof(RuntimeOps).GetMethod(nameof(RuntimeOps.ExpandoTryDeleteValue));
31 
32         private static readonly MethodInfo s_expandoPromoteClass =
33             typeof(RuntimeOps).GetMethod(nameof(RuntimeOps.ExpandoPromoteClass));
34 
35         private static readonly MethodInfo s_expandoCheckVersion =
36             typeof(RuntimeOps).GetMethod(nameof(RuntimeOps.ExpandoCheckVersion));
37 
38         internal readonly object LockObject;                          // the read-only field is used for locking the Expando object
39         private ExpandoData _data;                                    // the data currently being held by the Expando object
40         private int _count;                                           // the count of available members
41 
42         internal static readonly object Uninitialized = new object(); // A marker object used to identify that a value is uninitialized.
43 
44         internal const int AmbiguousMatchFound = -2;        // The value is used to indicate there exists ambiguous match in the Expando object
45         internal const int NoMatch = -1;                    // The value is used to indicate there is no matching member
46 
47         private PropertyChangedEventHandler _propertyChanged;
48 
49         /// <summary>
50         /// Creates a new ExpandoObject with no members.
51         /// </summary>
ExpandoObject()52         public ExpandoObject()
53         {
54             _data = ExpandoData.Empty;
55             LockObject = new object();
56         }
57 
58         #region Get/Set/Delete Helpers
59 
60         /// <summary>
61         /// Try to get the data stored for the specified class at the specified index.  If the
62         /// class has changed a full lookup for the slot will be performed and the correct
63         /// value will be retrieved.
64         /// </summary>
TryGetValue(object indexClass, int index, string name, bool ignoreCase, out object value)65         internal bool TryGetValue(object indexClass, int index, string name, bool ignoreCase, out object value)
66         {
67             // read the data now.  The data is immutable so we get a consistent view.
68             // If there's a concurrent writer they will replace data and it just appears
69             // that we won the race
70             ExpandoData data = _data;
71             if (data.Class != indexClass || ignoreCase)
72             {
73                 /* Re-search for the index matching the name here if
74                  *  1) the class has changed, we need to get the correct index and return
75                  *  the value there.
76                  *  2) the search is case insensitive:
77                  *      a. the member specified by index may be deleted, but there might be other
78                  *      members matching the name if the binder is case insensitive.
79                  *      b. the member that exactly matches the name didn't exist before and exists now,
80                  *      need to find the exact match.
81                  */
82                 index = data.Class.GetValueIndex(name, ignoreCase, this);
83                 if (index == ExpandoObject.AmbiguousMatchFound)
84                 {
85                     throw System.Linq.Expressions.Error.AmbiguousMatchInExpandoObject(name);
86                 }
87             }
88 
89             if (index == ExpandoObject.NoMatch)
90             {
91                 value = null;
92                 return false;
93             }
94 
95             // Capture the value into a temp, so it doesn't get mutated after we check
96             // for Uninitialized.
97             object temp = data[index];
98             if (temp == Uninitialized)
99             {
100                 value = null;
101                 return false;
102             }
103 
104             // index is now known to be correct
105             value = temp;
106             return true;
107         }
108 
109         /// <summary>
110         /// Sets the data for the specified class at the specified index.  If the class has
111         /// changed then a full look for the slot will be performed.  If the new class does
112         /// not have the provided slot then the Expando's class will change. Only case sensitive
113         /// setter is supported in ExpandoObject.
114         /// </summary>
TrySetValue(object indexClass, int index, object value, string name, bool ignoreCase, bool add)115         internal void TrySetValue(object indexClass, int index, object value, string name, bool ignoreCase, bool add)
116         {
117             ExpandoData data;
118             object oldValue;
119 
120             lock (LockObject)
121             {
122                 data = _data;
123 
124                 if (data.Class != indexClass || ignoreCase)
125                 {
126                     // The class has changed or we are doing a case-insensitive search,
127                     // we need to get the correct index and set the value there.  If we
128                     // don't have the value then we need to promote the class - that
129                     // should only happen when we have multiple concurrent writers.
130                     index = data.Class.GetValueIndex(name, ignoreCase, this);
131                     if (index == ExpandoObject.AmbiguousMatchFound)
132                     {
133                         throw System.Linq.Expressions.Error.AmbiguousMatchInExpandoObject(name);
134                     }
135                     if (index == ExpandoObject.NoMatch)
136                     {
137                         // Before creating a new class with the new member, need to check
138                         // if there is the exact same member but is deleted. We should reuse
139                         // the class if there is such a member.
140                         int exactMatch = ignoreCase ?
141                             data.Class.GetValueIndexCaseSensitive(name) :
142                             index;
143                         if (exactMatch != ExpandoObject.NoMatch)
144                         {
145                             Debug.Assert(data[exactMatch] == Uninitialized);
146                             index = exactMatch;
147                         }
148                         else
149                         {
150                             ExpandoClass newClass = data.Class.FindNewClass(name);
151                             data = PromoteClassCore(data.Class, newClass);
152                             // After the class promotion, there must be an exact match,
153                             // so we can do case-sensitive search here.
154                             index = data.Class.GetValueIndexCaseSensitive(name);
155                             Debug.Assert(index != ExpandoObject.NoMatch);
156                         }
157                     }
158                 }
159 
160                 // Setting an uninitialized member increases the count of available members
161                 oldValue = data[index];
162                 if (oldValue == Uninitialized)
163                 {
164                     _count++;
165                 }
166                 else if (add)
167                 {
168                     throw System.Linq.Expressions.Error.SameKeyExistsInExpando(name);
169                 }
170 
171                 data[index] = value;
172             }
173 
174             // Notify property changed outside the lock
175             PropertyChangedEventHandler propertyChanged = _propertyChanged;
176             if (propertyChanged != null && value != oldValue)
177             {
178                 propertyChanged(this, new PropertyChangedEventArgs(data.Class.Keys[index]));
179             }
180         }
181 
182         /// <summary>
183         /// Deletes the data stored for the specified class at the specified index.
184         /// </summary>
TryDeleteValue(object indexClass, int index, string name, bool ignoreCase, object deleteValue)185         internal bool TryDeleteValue(object indexClass, int index, string name, bool ignoreCase, object deleteValue)
186         {
187             ExpandoData data;
188             lock (LockObject)
189             {
190                 data = _data;
191 
192                 if (data.Class != indexClass || ignoreCase)
193                 {
194                     // the class has changed or we are doing a case-insensitive search,
195                     // we need to get the correct index.  If there is no associated index
196                     // we simply can't have the value and we return false.
197                     index = data.Class.GetValueIndex(name, ignoreCase, this);
198                     if (index == ExpandoObject.AmbiguousMatchFound)
199                     {
200                         throw System.Linq.Expressions.Error.AmbiguousMatchInExpandoObject(name);
201                     }
202                 }
203                 if (index == ExpandoObject.NoMatch)
204                 {
205                     return false;
206                 }
207 
208                 object oldValue = data[index];
209                 if (oldValue == Uninitialized)
210                 {
211                     return false;
212                 }
213 
214                 // Make sure the value matches, if requested.
215                 //
216                 // It's a shame we have to call Equals with the lock held but
217                 // there doesn't seem to be a good way around that, and
218                 // ConcurrentDictionary in mscorlib does the same thing.
219                 if (deleteValue != Uninitialized && !object.Equals(oldValue, deleteValue))
220                 {
221                     return false;
222                 }
223 
224                 data[index] = Uninitialized;
225 
226                 // Deleting an available member decreases the count of available members
227                 _count--;
228             }
229 
230             // Notify property changed outside the lock
231             _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(data.Class.Keys[index]));
232 
233             return true;
234         }
235 
236         /// <summary>
237         /// Returns true if the member at the specified index has been deleted,
238         /// otherwise false. Call this function holding the lock.
239         /// </summary>
IsDeletedMember(int index)240         internal bool IsDeletedMember(int index)
241         {
242             ContractUtils.AssertLockHeld(LockObject);
243             Debug.Assert(index >= 0 && index <= _data.Length);
244 
245             if (index == _data.Length)
246             {
247                 // The member is a newly added by SetMemberBinder and not in data yet
248                 return false;
249             }
250 
251             return _data[index] == ExpandoObject.Uninitialized;
252         }
253 
254         /// <summary>
255         /// Exposes the ExpandoClass which we've associated with this
256         /// Expando object.  Used for type checks in rules.
257         /// </summary>
258         internal ExpandoClass Class => _data.Class;
259 
260         /// <summary>
261         /// Promotes the class from the old type to the new type and returns the new
262         /// ExpandoData object.
263         /// </summary>
PromoteClassCore(ExpandoClass oldClass, ExpandoClass newClass)264         private ExpandoData PromoteClassCore(ExpandoClass oldClass, ExpandoClass newClass)
265         {
266             Debug.Assert(oldClass != newClass);
267             ContractUtils.AssertLockHeld(LockObject);
268 
269             if (_data.Class == oldClass)
270             {
271                 _data = _data.UpdateClass(newClass);
272             }
273 
274             return _data;
275         }
276 
277         /// <summary>
278         /// Internal helper to promote a class.  Called from our RuntimeOps helper.  This
279         /// version simply doesn't expose the ExpandoData object which is a private
280         /// data structure.
281         /// </summary>
PromoteClass(object oldClass, object newClass)282         internal void PromoteClass(object oldClass, object newClass)
283         {
284             lock (LockObject)
285             {
286                 PromoteClassCore((ExpandoClass)oldClass, (ExpandoClass)newClass);
287             }
288         }
289 
290         #endregion
291 
292         #region IDynamicMetaObjectProvider Members
293 
IDynamicMetaObjectProvider.GetMetaObject(Expression parameter)294         DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(Expression parameter)
295         {
296             return new MetaExpando(parameter, this);
297         }
298 
299         #endregion
300 
301         #region Helper methods
TryAddMember(string key, object value)302         private void TryAddMember(string key, object value)
303         {
304             ContractUtils.RequiresNotNull(key, nameof(key));
305             // Pass null to the class, which forces lookup.
306             TrySetValue(null, -1, value, key, ignoreCase: false, add: true);
307         }
308 
TryGetValueForKey(string key, out object value)309         private bool TryGetValueForKey(string key, out object value)
310         {
311             // Pass null to the class, which forces lookup.
312             return TryGetValue(null, -1, key, ignoreCase: false, value: out value);
313         }
314 
ExpandoContainsKey(string key)315         private bool ExpandoContainsKey(string key)
316         {
317             ContractUtils.AssertLockHeld(LockObject);
318             return _data.Class.GetValueIndexCaseSensitive(key) >= 0;
319         }
320 
321         // We create a non-generic type for the debug view for each different collection type
322         // that uses DebuggerTypeProxy, instead of defining a generic debug view type and
323         // using different instantiations. The reason for this is that support for generics
324         // with using DebuggerTypeProxy is limited. For C#, DebuggerTypeProxy supports only
325         // open types (from MSDN http://msdn.microsoft.com/en-us/library/d8eyd8zc.aspx).
326         private sealed class KeyCollectionDebugView
327         {
328             private readonly ICollection<string> _collection;
329 
KeyCollectionDebugView(ICollection<string> collection)330             public KeyCollectionDebugView(ICollection<string> collection)
331             {
332                 ContractUtils.RequiresNotNull(collection, nameof(collection));
333                 _collection = collection;
334             }
335 
336             [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
337             public string[] Items
338             {
339                 get
340                 {
341                     string[] items = new string[_collection.Count];
342                     _collection.CopyTo(items, 0);
343                     return items;
344                 }
345             }
346         }
347 
348         [DebuggerTypeProxy(typeof(KeyCollectionDebugView))]
349         [DebuggerDisplay("Count = {Count}")]
350         private class KeyCollection : ICollection<string>
351         {
352             private readonly ExpandoObject _expando;
353             private readonly int _expandoVersion;
354             private readonly int _expandoCount;
355             private readonly ExpandoData _expandoData;
356 
KeyCollection(ExpandoObject expando)357             internal KeyCollection(ExpandoObject expando)
358             {
359                 lock (expando.LockObject)
360                 {
361                     _expando = expando;
362                     _expandoVersion = expando._data.Version;
363                     _expandoCount = expando._count;
364                     _expandoData = expando._data;
365                 }
366             }
367 
CheckVersion()368             private void CheckVersion()
369             {
370                 if (_expando._data.Version != _expandoVersion || _expandoData != _expando._data)
371                 {
372                     //the underlying expando object has changed
373                     throw System.Linq.Expressions.Error.CollectionModifiedWhileEnumerating();
374                 }
375             }
376 
377             #region ICollection<string> Members
378 
Add(string item)379             public void Add(string item)
380             {
381                 throw System.Linq.Expressions.Error.CollectionReadOnly();
382             }
383 
Clear()384             public void Clear()
385             {
386                 throw System.Linq.Expressions.Error.CollectionReadOnly();
387             }
388 
Contains(string item)389             public bool Contains(string item)
390             {
391                 lock (_expando.LockObject)
392                 {
393                     CheckVersion();
394                     return _expando.ExpandoContainsKey(item);
395                 }
396             }
397 
CopyTo(string[] array, int arrayIndex)398             public void CopyTo(string[] array, int arrayIndex)
399             {
400                 ContractUtils.RequiresNotNull(array, nameof(array));
401                 ContractUtils.RequiresArrayRange(array, arrayIndex, _expandoCount, nameof(arrayIndex), nameof(Count));
402                 lock (_expando.LockObject)
403                 {
404                     CheckVersion();
405                     ExpandoData data = _expando._data;
406                     for (int i = 0; i < data.Class.Keys.Length; i++)
407                     {
408                         if (data[i] != Uninitialized)
409                         {
410                             array[arrayIndex++] = data.Class.Keys[i];
411                         }
412                     }
413                 }
414             }
415 
416             public int Count
417             {
418                 get
419                 {
420                     CheckVersion();
421                     return _expandoCount;
422                 }
423             }
424 
425             public bool IsReadOnly => true;
426 
Remove(string item)427             public bool Remove(string item)
428             {
429                 throw System.Linq.Expressions.Error.CollectionReadOnly();
430             }
431 
432             #endregion
433 
434             #region IEnumerable<string> Members
435 
GetEnumerator()436             public IEnumerator<string> GetEnumerator()
437             {
438                 for (int i = 0, n = _expandoData.Class.Keys.Length; i < n; i++)
439                 {
440                     CheckVersion();
441                     if (_expandoData[i] != Uninitialized)
442                     {
443                         yield return _expandoData.Class.Keys[i];
444                     }
445                 }
446             }
447 
448             #endregion
449 
450             #region IEnumerable Members
451 
System.Collections.IEnumerable.GetEnumerator()452             System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
453             {
454                 return GetEnumerator();
455             }
456 
457             #endregion
458         }
459 
460         // We create a non-generic type for the debug view for each different collection type
461         // that uses DebuggerTypeProxy, instead of defining a generic debug view type and
462         // using different instantiations. The reason for this is that support for generics
463         // with using DebuggerTypeProxy is limited. For C#, DebuggerTypeProxy supports only
464         // open types (from MSDN http://msdn.microsoft.com/en-us/library/d8eyd8zc.aspx).
465         private sealed class ValueCollectionDebugView
466         {
467             private readonly ICollection<object> _collection;
468 
ValueCollectionDebugView(ICollection<object> collection)469             public ValueCollectionDebugView(ICollection<object> collection)
470             {
471                 ContractUtils.RequiresNotNull(collection, nameof(collection));
472                 _collection = collection;
473             }
474 
475             [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
476             public object[] Items
477             {
478                 get
479                 {
480                     object[] items = new object[_collection.Count];
481                     _collection.CopyTo(items, 0);
482                     return items;
483                 }
484             }
485         }
486 
487         [DebuggerTypeProxy(typeof(ValueCollectionDebugView))]
488         [DebuggerDisplay("Count = {Count}")]
489         private class ValueCollection : ICollection<object>
490         {
491             private readonly ExpandoObject _expando;
492             private readonly int _expandoVersion;
493             private readonly int _expandoCount;
494             private readonly ExpandoData _expandoData;
495 
ValueCollection(ExpandoObject expando)496             internal ValueCollection(ExpandoObject expando)
497             {
498                 lock (expando.LockObject)
499                 {
500                     _expando = expando;
501                     _expandoVersion = expando._data.Version;
502                     _expandoCount = expando._count;
503                     _expandoData = expando._data;
504                 }
505             }
506 
CheckVersion()507             private void CheckVersion()
508             {
509                 if (_expando._data.Version != _expandoVersion || _expandoData != _expando._data)
510                 {
511                     //the underlying expando object has changed
512                     throw System.Linq.Expressions.Error.CollectionModifiedWhileEnumerating();
513                 }
514             }
515 
516             #region ICollection<string> Members
517 
Add(object item)518             public void Add(object item)
519             {
520                 throw System.Linq.Expressions.Error.CollectionReadOnly();
521             }
522 
Clear()523             public void Clear()
524             {
525                 throw System.Linq.Expressions.Error.CollectionReadOnly();
526             }
527 
Contains(object item)528             public bool Contains(object item)
529             {
530                 lock (_expando.LockObject)
531                 {
532                     CheckVersion();
533 
534                     ExpandoData data = _expando._data;
535                     for (int i = 0; i < data.Class.Keys.Length; i++)
536                     {
537                         // See comment in TryDeleteValue; it's okay to call
538                         // object.Equals with the lock held.
539                         if (object.Equals(data[i], item))
540                         {
541                             return true;
542                         }
543                     }
544                     return false;
545                 }
546             }
547 
CopyTo(object[] array, int arrayIndex)548             public void CopyTo(object[] array, int arrayIndex)
549             {
550                 ContractUtils.RequiresNotNull(array, nameof(array));
551                 ContractUtils.RequiresArrayRange(array, arrayIndex, _expandoCount, nameof(arrayIndex), nameof(Count));
552                 lock (_expando.LockObject)
553                 {
554                     CheckVersion();
555                     ExpandoData data = _expando._data;
556                     for (int i = 0; i < data.Class.Keys.Length; i++)
557                     {
558                         if (data[i] != Uninitialized)
559                         {
560                             array[arrayIndex++] = data[i];
561                         }
562                     }
563                 }
564             }
565 
566             public int Count
567             {
568                 get
569                 {
570                     CheckVersion();
571                     return _expandoCount;
572                 }
573             }
574 
575             public bool IsReadOnly => true;
576 
Remove(object item)577             public bool Remove(object item)
578             {
579                 throw System.Linq.Expressions.Error.CollectionReadOnly();
580             }
581 
582             #endregion
583 
584             #region IEnumerable<string> Members
585 
GetEnumerator()586             public IEnumerator<object> GetEnumerator()
587             {
588                 ExpandoData data = _expando._data;
589                 for (int i = 0; i < data.Class.Keys.Length; i++)
590                 {
591                     CheckVersion();
592                     // Capture the value into a temp so we don't inadvertently
593                     // return Uninitialized.
594                     object temp = data[i];
595                     if (temp != Uninitialized)
596                     {
597                         yield return temp;
598                     }
599                 }
600             }
601 
602             #endregion
603 
604             #region IEnumerable Members
605 
System.Collections.IEnumerable.GetEnumerator()606             System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
607             {
608                 return GetEnumerator();
609             }
610 
611             #endregion
612         }
613 
614         #endregion
615 
616         #region IDictionary<string, object> Members
617 
618         ICollection<string> IDictionary<string, object>.Keys => new KeyCollection(this);
619 
620         ICollection<object> IDictionary<string, object>.Values => new ValueCollection(this);
621 
622         object IDictionary<string, object>.this[string key]
623         {
624             get
625             {
626                 object value;
627                 if (!TryGetValueForKey(key, out value))
628                 {
629                     throw System.Linq.Expressions.Error.KeyDoesNotExistInExpando(key);
630                 }
631                 return value;
632             }
633             set
634             {
635                 ContractUtils.RequiresNotNull(key, nameof(key));
636                 // Pass null to the class, which forces lookup.
637                 TrySetValue(null, -1, value, key, ignoreCase: false, add: false);
638             }
639         }
640 
Add(string key, object value)641         void IDictionary<string, object>.Add(string key, object value)
642         {
643             this.TryAddMember(key, value);
644         }
645 
ContainsKey(string key)646         bool IDictionary<string, object>.ContainsKey(string key)
647         {
648             ContractUtils.RequiresNotNull(key, nameof(key));
649 
650             ExpandoData data = _data;
651             int index = data.Class.GetValueIndexCaseSensitive(key);
652             return index >= 0 && data[index] != Uninitialized;
653         }
654 
Remove(string key)655         bool IDictionary<string, object>.Remove(string key)
656         {
657             ContractUtils.RequiresNotNull(key, nameof(key));
658             // Pass null to the class, which forces lookup.
659             return TryDeleteValue(null, -1, key, ignoreCase: false, deleteValue: Uninitialized);
660         }
661 
TryGetValue(string key, out object value)662         bool IDictionary<string, object>.TryGetValue(string key, out object value)
663         {
664             return TryGetValueForKey(key, out value);
665         }
666 
667         #endregion
668 
669         #region ICollection<KeyValuePair<string, object>> Members
670 
671         int ICollection<KeyValuePair<string, object>>.Count => _count;
672 
673         bool ICollection<KeyValuePair<string, object>>.IsReadOnly => false;
674 
Add(KeyValuePair<string, object> item)675         void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
676         {
677             TryAddMember(item.Key, item.Value);
678         }
679 
Clear()680         void ICollection<KeyValuePair<string, object>>.Clear()
681         {
682             // We remove both class and data!
683             ExpandoData data;
684             lock (LockObject)
685             {
686                 data = _data;
687                 _data = ExpandoData.Empty;
688                 _count = 0;
689             }
690 
691             // Notify property changed for all properties.
692             var propertyChanged = _propertyChanged;
693             if (propertyChanged != null)
694             {
695                 for (int i = 0, n = data.Class.Keys.Length; i < n; i++)
696                 {
697                     if (data[i] != Uninitialized)
698                     {
699                         propertyChanged(this, new PropertyChangedEventArgs(data.Class.Keys[i]));
700                     }
701                 }
702             }
703         }
704 
Contains(KeyValuePair<string, object> item)705         bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
706         {
707             object value;
708             if (!TryGetValueForKey(item.Key, out value))
709             {
710                 return false;
711             }
712 
713             return object.Equals(value, item.Value);
714         }
715 
CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)716         void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
717         {
718             ContractUtils.RequiresNotNull(array, nameof(array));
719 
720             // We want this to be atomic and not throw, though we must do the range checks inside this lock.
721             lock (LockObject)
722             {
723                 ContractUtils.RequiresArrayRange(array, arrayIndex, _count, nameof(arrayIndex), nameof(ICollection<KeyValuePair<string, object>>.Count));
724                 foreach (KeyValuePair<string, object> item in this)
725                 {
726                     array[arrayIndex++] = item;
727                 }
728             }
729         }
730 
Remove(KeyValuePair<string, object> item)731         bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
732         {
733             return TryDeleteValue(null, -1, item.Key, ignoreCase: false, deleteValue: item.Value);
734         }
735 
736         #endregion
737 
738         #region IEnumerable<KeyValuePair<string, object>> Member
739 
GetEnumerator()740         IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
741         {
742             ExpandoData data = _data;
743             return GetExpandoEnumerator(data, data.Version);
744         }
745 
System.Collections.IEnumerable.GetEnumerator()746         System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
747         {
748             ExpandoData data = _data;
749             return GetExpandoEnumerator(data, data.Version);
750         }
751 
752         // Note: takes the data and version as parameters so they will be
753         // captured before the first call to MoveNext().
GetExpandoEnumerator(ExpandoData data, int version)754         private IEnumerator<KeyValuePair<string, object>> GetExpandoEnumerator(ExpandoData data, int version)
755         {
756             for (int i = 0; i < data.Class.Keys.Length; i++)
757             {
758                 if (_data.Version != version || data != _data)
759                 {
760                     // The underlying expando object has changed:
761                     // 1) the version of the expando data changed
762                     // 2) the data object is changed
763                     throw System.Linq.Expressions.Error.CollectionModifiedWhileEnumerating();
764                 }
765                 // Capture the value into a temp so we don't inadvertently
766                 // return Uninitialized.
767                 object temp = data[i];
768                 if (temp != Uninitialized)
769                 {
770                     yield return new KeyValuePair<string, object>(data.Class.Keys[i], temp);
771                 }
772             }
773         }
774 
775         #endregion
776 
777         #region MetaExpando
778 
779         private class MetaExpando : DynamicMetaObject
780         {
MetaExpando(Expression expression, ExpandoObject value)781             public MetaExpando(Expression expression, ExpandoObject value)
782                 : base(expression, BindingRestrictions.Empty, value)
783             {
784             }
785 
BindGetOrInvokeMember(DynamicMetaObjectBinder binder, string name, bool ignoreCase, DynamicMetaObject fallback, Func<DynamicMetaObject, DynamicMetaObject> fallbackInvoke)786             private DynamicMetaObject BindGetOrInvokeMember(DynamicMetaObjectBinder binder, string name, bool ignoreCase, DynamicMetaObject fallback, Func<DynamicMetaObject, DynamicMetaObject> fallbackInvoke)
787             {
788                 ExpandoClass klass = Value.Class;
789 
790                 //try to find the member, including the deleted members
791                 int index = klass.GetValueIndex(name, ignoreCase, Value);
792 
793                 ParameterExpression value = Expression.Parameter(typeof(object), "value");
794 
795                 Expression tryGetValue = Expression.Call(
796                     s_expandoTryGetValue,
797                     GetLimitedSelf(),
798                     Expression.Constant(klass, typeof(object)),
799                     AstUtils.Constant(index),
800                     Expression.Constant(name),
801                     AstUtils.Constant(ignoreCase),
802                     value
803                 );
804 
805                 var result = new DynamicMetaObject(value, BindingRestrictions.Empty);
806                 if (fallbackInvoke != null)
807                 {
808                     result = fallbackInvoke(result);
809                 }
810 
811                 result = new DynamicMetaObject(
812                     Expression.Block(
813                         new TrueReadOnlyCollection<ParameterExpression>(value),
814                         new TrueReadOnlyCollection<Expression>(
815                             Expression.Condition(
816                                 tryGetValue,
817                                 result.Expression,
818                                 fallback.Expression,
819                                 typeof(object)
820                             )
821                         )
822                     ),
823                     result.Restrictions.Merge(fallback.Restrictions)
824                 );
825 
826                 return AddDynamicTestAndDefer(binder, Value.Class, null, result);
827             }
828 
BindGetMember(GetMemberBinder binder)829             public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
830             {
831                 ContractUtils.RequiresNotNull(binder, nameof(binder));
832                 return BindGetOrInvokeMember(
833                     binder,
834                     binder.Name,
835                     binder.IgnoreCase,
836                     binder.FallbackGetMember(this),
837                     null
838                 );
839             }
840 
BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args)841             public override DynamicMetaObject BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args)
842             {
843                 ContractUtils.RequiresNotNull(binder, nameof(binder));
844                 return BindGetOrInvokeMember(
845                     binder,
846                     binder.Name,
847                     binder.IgnoreCase,
848                     binder.FallbackInvokeMember(this, args),
849                     value => binder.FallbackInvoke(value, args, null)
850                 );
851             }
852 
BindSetMember(SetMemberBinder binder, DynamicMetaObject value)853             public override DynamicMetaObject BindSetMember(SetMemberBinder binder, DynamicMetaObject value)
854             {
855                 ContractUtils.RequiresNotNull(binder, nameof(binder));
856                 ContractUtils.RequiresNotNull(value, nameof(value));
857 
858                 ExpandoClass klass;
859                 int index;
860 
861                 ExpandoClass originalClass = GetClassEnsureIndex(binder.Name, binder.IgnoreCase, Value, out klass, out index);
862 
863                 return AddDynamicTestAndDefer(
864                     binder,
865                     klass,
866                     originalClass,
867                     new DynamicMetaObject(
868                         Expression.Call(
869                             s_expandoTrySetValue,
870                             GetLimitedSelf(),
871                             Expression.Constant(klass, typeof(object)),
872                             AstUtils.Constant(index),
873                             Expression.Convert(value.Expression, typeof(object)),
874                             Expression.Constant(binder.Name),
875                             AstUtils.Constant(binder.IgnoreCase)
876                         ),
877                         BindingRestrictions.Empty
878                     )
879                 );
880             }
881 
BindDeleteMember(DeleteMemberBinder binder)882             public override DynamicMetaObject BindDeleteMember(DeleteMemberBinder binder)
883             {
884                 ContractUtils.RequiresNotNull(binder, nameof(binder));
885 
886                 int index = Value.Class.GetValueIndex(binder.Name, binder.IgnoreCase, Value);
887 
888                 Expression tryDelete = Expression.Call(
889                     s_expandoTryDeleteValue,
890                     GetLimitedSelf(),
891                     Expression.Constant(Value.Class, typeof(object)),
892                     AstUtils.Constant(index),
893                     Expression.Constant(binder.Name),
894                     AstUtils.Constant(binder.IgnoreCase)
895                 );
896                 DynamicMetaObject fallback = binder.FallbackDeleteMember(this);
897 
898                 DynamicMetaObject target = new DynamicMetaObject(
899                     Expression.IfThen(Expression.Not(tryDelete), fallback.Expression),
900                     fallback.Restrictions
901                 );
902 
903                 return AddDynamicTestAndDefer(binder, Value.Class, null, target);
904             }
905 
GetDynamicMemberNames()906             public override IEnumerable<string> GetDynamicMemberNames()
907             {
908                 var expandoData = Value._data;
909                 var klass = expandoData.Class;
910                 for (int i = 0; i < klass.Keys.Length; i++)
911                 {
912                     object val = expandoData[i];
913                     if (val != ExpandoObject.Uninitialized)
914                     {
915                         yield return klass.Keys[i];
916                     }
917                 }
918             }
919 
920             /// <summary>
921             /// Adds a dynamic test which checks if the version has changed.  The test is only necessary for
922             /// performance as the methods will do the correct thing if called with an incorrect version.
923             /// </summary>
AddDynamicTestAndDefer(DynamicMetaObjectBinder binder, ExpandoClass klass, ExpandoClass originalClass, DynamicMetaObject succeeds)924             private DynamicMetaObject AddDynamicTestAndDefer(DynamicMetaObjectBinder binder, ExpandoClass klass, ExpandoClass originalClass, DynamicMetaObject succeeds)
925             {
926                 Expression ifTestSucceeds = succeeds.Expression;
927                 if (originalClass != null)
928                 {
929                     // we are accessing a member which has not yet been defined on this class.
930                     // We force a class promotion after the type check.  If the class changes the
931                     // promotion will fail and the set/delete will do a full lookup using the new
932                     // class to discover the name.
933                     Debug.Assert(originalClass != klass);
934 
935                     ifTestSucceeds = Expression.Block(
936                         Expression.Call(
937                             null,
938                             s_expandoPromoteClass,
939                             GetLimitedSelf(),
940                             Expression.Constant(originalClass, typeof(object)),
941                             Expression.Constant(klass, typeof(object))
942                         ),
943                         succeeds.Expression
944                     );
945                 }
946 
947                 return new DynamicMetaObject(
948                     Expression.Condition(
949                         Expression.Call(
950                             null,
951                             s_expandoCheckVersion,
952                             GetLimitedSelf(),
953                             Expression.Constant(originalClass ?? klass, typeof(object))
954                         ),
955                         ifTestSucceeds,
956                         binder.GetUpdateExpression(ifTestSucceeds.Type)
957                     ),
958                     GetRestrictions().Merge(succeeds.Restrictions)
959                 );
960             }
961 
962             /// <summary>
963             /// Gets the class and the index associated with the given name.  Does not update the expando object.  Instead
964             /// this returns both the original and desired new class.  A rule is created which includes the test for the
965             /// original class, the promotion to the new class, and the set/delete based on the class post-promotion.
966             /// </summary>
GetClassEnsureIndex(string name, bool caseInsensitive, ExpandoObject obj, out ExpandoClass klass, out int index)967             private ExpandoClass GetClassEnsureIndex(string name, bool caseInsensitive, ExpandoObject obj, out ExpandoClass klass, out int index)
968             {
969                 ExpandoClass originalClass = Value.Class;
970 
971                 index = originalClass.GetValueIndex(name, caseInsensitive, obj);
972                 if (index == ExpandoObject.AmbiguousMatchFound)
973                 {
974                     klass = originalClass;
975                     return null;
976                 }
977                 if (index == ExpandoObject.NoMatch)
978                 {
979                     // go ahead and find a new class now...
980                     ExpandoClass newClass = originalClass.FindNewClass(name);
981 
982                     klass = newClass;
983                     index = newClass.GetValueIndexCaseSensitive(name);
984 
985                     Debug.Assert(index != ExpandoObject.NoMatch);
986                     return originalClass;
987                 }
988                 else
989                 {
990                     klass = originalClass;
991                     return null;
992                 }
993             }
994 
995             /// <summary>
996             /// Returns our Expression converted to our known LimitType
997             /// </summary>
GetLimitedSelf()998             private Expression GetLimitedSelf()
999             {
1000                 if (TypeUtils.AreEquivalent(Expression.Type, LimitType))
1001                 {
1002                     return Expression;
1003                 }
1004                 return Expression.Convert(Expression, LimitType);
1005             }
1006 
1007             /// <summary>
1008             /// Returns a Restrictions object which includes our current restrictions merged
1009             /// with a restriction limiting our type
1010             /// </summary>
GetRestrictions()1011             private BindingRestrictions GetRestrictions()
1012             {
1013                 Debug.Assert(Restrictions == BindingRestrictions.Empty, "We don't merge, restrictions are always empty");
1014 
1015                 return BindingRestrictions.GetTypeRestriction(this);
1016             }
1017 
1018             public new ExpandoObject Value => (ExpandoObject)base.Value;
1019         }
1020 
1021         #endregion
1022 
1023         #region ExpandoData
1024 
1025         /// <summary>
1026         /// Stores the class and the data associated with the class as one atomic
1027         /// pair.  This enables us to do a class check in a thread safe manner w/o
1028         /// requiring locks.
1029         /// </summary>
1030         private class ExpandoData
1031         {
1032             internal static ExpandoData Empty = new ExpandoData();
1033 
1034             /// <summary>
1035             /// the dynamically assigned class associated with the Expando object
1036             /// </summary>
1037             internal readonly ExpandoClass Class;
1038 
1039             /// <summary>
1040             /// data stored in the expando object, key names are stored in the class.
1041             ///
1042             /// Expando._data must be locked when mutating the value.  Otherwise a copy of it
1043             /// could be made and lose values.
1044             /// </summary>
1045             private readonly object[] _dataArray;
1046 
1047             /// <summary>
1048             /// Indexer for getting/setting the data
1049             /// </summary>
1050             internal object this[int index]
1051             {
1052                 get
1053                 {
1054                     return _dataArray[index];
1055                 }
1056                 set
1057                 {
1058                     //when the array is updated, version increases, even the new value is the same
1059                     //as previous. Dictionary type has the same behavior.
1060                     _version++;
1061                     _dataArray[index] = value;
1062                 }
1063             }
1064 
1065             internal int Version => _version;
1066 
1067             internal int Length => _dataArray.Length;
1068 
1069             /// <summary>
1070             /// Constructs an empty ExpandoData object with the empty class and no data.
1071             /// </summary>
ExpandoData()1072             private ExpandoData()
1073             {
1074                 Class = ExpandoClass.Empty;
1075                 _dataArray = Array.Empty<object>();
1076             }
1077 
1078             /// <summary>
1079             /// the version of the ExpandoObject that tracks set and delete operations
1080             /// </summary>
1081             private int _version;
1082 
1083             /// <summary>
1084             /// Constructs a new ExpandoData object with the specified class and data.
1085             /// </summary>
ExpandoData(ExpandoClass klass, object[] data, int version)1086             internal ExpandoData(ExpandoClass klass, object[] data, int version)
1087             {
1088                 Class = klass;
1089                 _dataArray = data;
1090                 _version = version;
1091             }
1092 
1093             /// <summary>
1094             /// Update the associated class and increases the storage for the data array if needed.
1095             /// </summary>
UpdateClass(ExpandoClass newClass)1096             internal ExpandoData UpdateClass(ExpandoClass newClass)
1097             {
1098                 if (_dataArray.Length >= newClass.Keys.Length)
1099                 {
1100                     // we have extra space in our buffer, just initialize it to Uninitialized.
1101                     this[newClass.Keys.Length - 1] = ExpandoObject.Uninitialized;
1102                     return new ExpandoData(newClass, _dataArray, _version);
1103                 }
1104                 else
1105                 {
1106                     // we've grown too much - we need a new object array
1107                     int oldLength = _dataArray.Length;
1108                     object[] arr = new object[GetAlignedSize(newClass.Keys.Length)];
1109                     Array.Copy(_dataArray, 0, arr, 0, _dataArray.Length);
1110                     ExpandoData newData = new ExpandoData(newClass, arr, _version);
1111                     newData[oldLength] = ExpandoObject.Uninitialized;
1112                     return newData;
1113                 }
1114             }
1115 
GetAlignedSize(int len)1116             private static int GetAlignedSize(int len)
1117             {
1118                 // the alignment of the array for storage of values (must be a power of two)
1119                 const int DataArrayAlignment = 8;
1120 
1121                 // round up and then mask off lower bits
1122                 return (len + (DataArrayAlignment - 1)) & (~(DataArrayAlignment - 1));
1123             }
1124         }
1125 
1126         #endregion
1127 
1128         #region INotifyPropertyChanged
1129 
1130         event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
1131         {
1132             add { _propertyChanged += value; }
1133             remove { _propertyChanged -= value; }
1134         }
1135 
1136         #endregion
1137     }
1138 }
1139 
1140 namespace System.Runtime.CompilerServices
1141 {
1142     //
1143     // Note: these helpers are kept as simple wrappers so they have a better
1144     // chance of being inlined.
1145     //
1146     public static partial class RuntimeOps
1147     {
1148         /// <summary>
1149         /// Gets the value of an item in an expando object.
1150         /// </summary>
1151         /// <param name="expando">The expando object.</param>
1152         /// <param name="indexClass">The class of the expando object.</param>
1153         /// <param name="index">The index of the member.</param>
1154         /// <param name="name">The name of the member.</param>
1155         /// <param name="ignoreCase">true if the name should be matched ignoring case; false otherwise.</param>
1156         /// <param name="value">The out parameter containing the value of the member.</param>
1157         /// <returns>True if the member exists in the expando object, otherwise false.</returns>
1158         [Obsolete("do not use this method", error: true), EditorBrowsable(EditorBrowsableState.Never)]
ExpandoTryGetValue(ExpandoObject expando, object indexClass, int index, string name, bool ignoreCase, out object value)1159         public static bool ExpandoTryGetValue(ExpandoObject expando, object indexClass, int index, string name, bool ignoreCase, out object value)
1160         {
1161             return expando.TryGetValue(indexClass, index, name, ignoreCase, out value);
1162         }
1163 
1164         /// <summary>
1165         /// Sets the value of an item in an expando object.
1166         /// </summary>
1167         /// <param name="expando">The expando object.</param>
1168         /// <param name="indexClass">The class of the expando object.</param>
1169         /// <param name="index">The index of the member.</param>
1170         /// <param name="value">The value of the member.</param>
1171         /// <param name="name">The name of the member.</param>
1172         /// <param name="ignoreCase">true if the name should be matched ignoring case; false otherwise.</param>
1173         /// <returns>
1174         /// Returns the index for the set member.
1175         /// </returns>
1176         [Obsolete("do not use this method", error: true), EditorBrowsable(EditorBrowsableState.Never)]
ExpandoTrySetValue(ExpandoObject expando, object indexClass, int index, object value, string name, bool ignoreCase)1177         public static object ExpandoTrySetValue(ExpandoObject expando, object indexClass, int index, object value, string name, bool ignoreCase)
1178         {
1179             expando.TrySetValue(indexClass, index, value, name, ignoreCase, false);
1180             return value;
1181         }
1182 
1183         /// <summary>
1184         /// Deletes the value of an item in an expando object.
1185         /// </summary>
1186         /// <param name="expando">The expando object.</param>
1187         /// <param name="indexClass">The class of the expando object.</param>
1188         /// <param name="index">The index of the member.</param>
1189         /// <param name="name">The name of the member.</param>
1190         /// <param name="ignoreCase">true if the name should be matched ignoring case; false otherwise.</param>
1191         /// <returns>true if the item was successfully removed; otherwise, false.</returns>
1192         [Obsolete("do not use this method", error: true), EditorBrowsable(EditorBrowsableState.Never)]
ExpandoTryDeleteValue(ExpandoObject expando, object indexClass, int index, string name, bool ignoreCase)1193         public static bool ExpandoTryDeleteValue(ExpandoObject expando, object indexClass, int index, string name, bool ignoreCase)
1194         {
1195             return expando.TryDeleteValue(indexClass, index, name, ignoreCase, ExpandoObject.Uninitialized);
1196         }
1197 
1198         /// <summary>
1199         /// Checks the version of the expando object.
1200         /// </summary>
1201         /// <param name="expando">The expando object.</param>
1202         /// <param name="version">The version to check.</param>
1203         /// <returns>true if the version is equal; otherwise, false.</returns>
1204         [Obsolete("do not use this method", error: true), EditorBrowsable(EditorBrowsableState.Never)]
ExpandoCheckVersion(ExpandoObject expando, object version)1205         public static bool ExpandoCheckVersion(ExpandoObject expando, object version)
1206         {
1207             return expando.Class == version;
1208         }
1209 
1210         /// <summary>
1211         /// Promotes an expando object from one class to a new class.
1212         /// </summary>
1213         /// <param name="expando">The expando object.</param>
1214         /// <param name="oldClass">The old class of the expando object.</param>
1215         /// <param name="newClass">The new class of the expando object.</param>
1216         [Obsolete("do not use this method", error: true), EditorBrowsable(EditorBrowsableState.Never)]
ExpandoPromoteClass(ExpandoObject expando, object oldClass, object newClass)1217         public static void ExpandoPromoteClass(ExpandoObject expando, object oldClass, object newClass)
1218         {
1219             expando.PromoteClass(oldClass, newClass);
1220         }
1221     }
1222 }
1223