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