using System.Dynamic; using System.Reflection; using Speckle.Newtonsoft.Json; using Speckle.Sdk.Host; namespace Speckle.Sdk.Models; /// /// Base class implementing a bunch of nice dynamic object methods, like adding and removing props dynamically. Makes c# feel like json. /// Originally adapted from Rick Strahl 🤘 /// https://weblog.west-wind.com/posts/2012/feb/08/creating-a-dynamic-extensible-c-expando-object /// public class DynamicBase : DynamicObject, IDynamicMetaObjectProvider { /// /// Default value for /// public const DynamicBaseMemberType DEFAULT_INCLUDE_MEMBERS = DynamicBaseMemberType.Instance | DynamicBaseMemberType.Dynamic; /// /// The actual property bag, where dynamically added props are stored. /// private readonly Dictionary _properties = new(); /// /// Sets and gets properties using the key accessor pattern. /// /// /// myObject["superProperty"] = 42; /// /// /// [IgnoreTheItem] public object? this[string key] { get { if (_properties.TryGetValue(key, out object? value)) { return value; } var pinfos = TypeLoader.GetBaseProperties(GetType()); var prop = pinfos.FirstOrDefault(p => p.Name == key); if (prop == null) { return null; } return prop.GetValue(this); } set { if (!IsPropNameValid(key, out string reason)) { throw new InvalidPropNameException(key, reason); } if (_properties.ContainsKey(key)) { _properties[key] = value; return; } var pinfos = TypeLoader.GetBaseProperties(GetType()); var prop = pinfos.FirstOrDefault(p => p.Name == key); if (prop == null) { _properties[key] = value; return; } try { prop.SetValue(this, value); } catch (Exception ex) when (!ex.IsFatal()) { throw new SpeckleException($"Failed to set value for {GetType().Name}.{prop.Name}", ex); } } } /// /// Creates a shallow copy of the current base object. /// This operation does NOT copy/duplicate the data inside each prop. /// The new object's property values will be pointers to the original object's property value. /// /// A shallow copy of the original object. public DynamicBase ShallowCopy() { Type type = GetType(); DynamicBase myDuplicate = (DynamicBase)( Activator.CreateInstance(type) ?? throw new SpeckleException($"Failed to create instance of {type.Name}") ); // Add dynamic members foreach (var kvp in _properties) { myDuplicate._properties[kvp.Key] = kvp.Value; } var pinfos = TypeLoader.GetBaseProperties(type).Where(x => !TypeLoader.IsObsolete(x)); foreach (var pi in pinfos) { if (pi.CanWrite) { try { pi.SetValue(myDuplicate, pi.GetValue(this)); } catch (Exception ex) when (!ex.IsFatal()) { throw new SpeckleException($"Failed to set value for {type.Name}.{pi.Name}", ex); } } } return myDuplicate; } /// /// /// Gets properties via the dot syntax. /// ((dynamic)myObject).superProperty; /// /// public override bool TryGetMember(GetMemberBinder binder, out object? result) { return _properties.TryGetValue(binder.Name, out result); } /// /// Sets properties via the dot syntax. ///
((dynamic)myObject).superProperty = something;
///
/// /// /// public override bool TrySetMember(SetMemberBinder binder, object? value) { var valid = IsPropNameValid(binder.Name, out _); if (valid) { _properties[binder.Name] = value; } return valid; } private static readonly char[] s_disallowedPropNameChars = { '.', '/' }; public static string RemoveDisallowedPropNameChars(string name) { foreach (char c in s_disallowedPropNameChars) { name = name.Replace(c, ' '); } return name; } //apparently used a lot so optimize the check public unsafe bool IsPropNameValid(string name, out string reason) { if (string.IsNullOrEmpty(name) || name.Equals("@", StringComparison.Ordinal)) { reason = "Found empty prop name"; return false; } if (name.StartsWith("@@", StringComparison.Ordinal)) { reason = "Only one leading '@' char is allowed. This signals the property value should be detached."; return false; } int len = name.Length; fixed (char* ptr = name) { for (int i = 0; i < len; i++) { for (int j = 0; j < s_disallowedPropNameChars.Length; j++) { if (s_disallowedPropNameChars[j] == ptr[i]) { reason = $"Prop with name '{name}' contains invalid characters. The following characters are not allowed: ./"; return false; } } } // talk to ptr[0] etc; DO NOT go outside of ptr[0] <---> ptr[len-1] } reason = string.Empty; return true; } /// /// Gets all of the property names on this class, dynamic or not. /// public override IEnumerable GetDynamicMemberNames() { var pinfos = TypeLoader.GetBaseProperties(GetType()); foreach (var pinfo in pinfos) { yield return pinfo.Name; } foreach (var kvp in _properties) { yield return kvp.Key; } } public static IEnumerable GetInstanceMembersNames(Type t) { var pinfos = TypeLoader.GetBaseProperties(t); foreach (var pinfo in pinfos) { yield return pinfo.Name; } } /// /// Gets the defined (typed) properties of this object. /// /// public IEnumerable GetInstanceMembers() { return GetInstanceMembers(GetType()); } public static IEnumerable GetInstanceMembers(Type t) { var pinfos = TypeLoader.GetBaseProperties(t); foreach (var pinfo in pinfos) { if (pinfo.Name != "Item") { yield return pinfo; } } } /// /// Gets the typed and dynamic properties. /// /// Specifies which members should be included in the resulting dictionary. Can be concatenated with "|" /// A dictionary containing the key's and values of the object. public Dictionary GetMembers(DynamicBaseMemberType includeMembers = DEFAULT_INCLUDE_MEMBERS) { // Initialize an empty dict var dic = new Dictionary(); // Add dynamic members if (includeMembers.HasFlag(DynamicBaseMemberType.Dynamic)) { dic = new Dictionary(_properties); } if (includeMembers.HasFlag(DynamicBaseMemberType.Instance)) { var pinfos = TypeLoader .GetBaseProperties(GetType()) .Where(x => { var hasObsolete = TypeLoader.IsObsolete(x); // If obsolete is false and prop has obsolete attr // OR // If schemaIgnored is true and prop has schemaIgnore attr return !(!includeMembers.HasFlag(DynamicBaseMemberType.Obsolete) && hasObsolete); }); foreach (var pi in pinfos) { if (!dic.ContainsKey(pi.Name)) //todo This is a TEMP FIX FOR #1969, and should be reverted after a proper fix is made! { dic.Add(pi.Name, pi.GetValue(this)); } } } return dic; } /// /// Gets the dynamically added property names only. /// /// [JsonIgnore] public IReadOnlyCollection DynamicPropertyKeys => _properties.Keys; } /// /// This attribute is used internally to hide the this[key]{get; set;} property from inner reflection on members. /// For more info see this discussion: https://speckle.community/t/why-do-i-keep-forgetting-base-objects-cant-use-item-as-a-dynamic-member/3246/5 /// [AttributeUsage(AttributeTargets.Property)] internal sealed class IgnoreTheItemAttribute : Attribute { }