Quantcast
Channel: Development With A Dot
Viewing all articles
Browse latest Browse all 404

Elastic Object Implementation in .NET

$
0
0

I think it was Anoop Madhusudanan (aka, amazedsaint) who first coined the term “Elastic Object”. He even built an implementation, which you can find on GitHub and NuGet, which is very nice. Basically, it’s a class that allows adding members to its instances dynamically; this is a widely known pattern in scripting languages such as JavaScript, but not so easy to implement in .NET.

Now, I wanted to have something like this but also add some custom features that were missing, so I started something from scratch – yes, for the good or for the bad, my code is totally different from amazedsaint’s). Of course, both leverage the dynamic type, that’s where the whole idea came from.

Here are some examples of what we can do:

dynamic obj = new ElasticObject();
obj.A = 1;    //obj["A"] = 1;
obj.A.B = "1";
var props = TypeDescriptor.GetProperties(obj);    //"A"
 
dynamic obj = new ElasticObject(new { A = 1 });
int i = obj.A;
int ni = ~obj.A;
int ii = --obj.A;
bool b = obj.A;
 
dynmic obj = new ElasticObject(1);
var clone = obj.Clone();
(obj as INotifyPropertyChanged).PropertyChanged += (s, e) => { };
 
dynamic obj = new ElasticObject();
obj.A = 1;    //obj["A"] = 1;
obj.A.B = "1";
var path = obj.A.B["$path"];      //"A.B"
var parent = obj.A.B["$parent"];  //obj.A
var value = obj.A.B["$value"];    //"1"
var type = obj.A.B["$type"];      //string

Basically, I Inherit from DynamicObject and I use an internal dictionary for storing all dynamically assigned values, which will, in turn, be also ElasticObjects. I also allow for a “self value”, which will be the ElasticObject’s main value, in case no additional properties are supplied. I also provide implementations for some typical .NET interfaces, like INotifyPropertyChanged and ICloneable, and supply my own TypeDescriptionProvider which takes into account the dynamic properties and also a custom TypeConverter.

There are some provided properties that grant us access to the ElasticObject’s internals:

  • $Root: the root ElasticObject instance;
  • $Parent: the parent ElasticObject instance for the current one;
  • $Path: the full property path from the current object until the root;
  • $Value: the self value;
  • $Type: the type of the self value.


Without further discussion, here is the code for my ElasticObject class:

[Serializable]
[TypeConverter(typeof(ElasticObjectTypeConverter))]
[TypeDescriptionProvider(typeof(ElasticObjectTypeDescriptionProvider))]
publicsealedclass ElasticObject : DynamicObject, IDictionary<String, Object>, ICloneable, INotifyPropertyChanged
{
privatestaticreadonly String [] SpecialKeys = new String[] { "$Path", "$Parent", "$Root", "$Value", "$Type" };
privatereadonly IDictionary<String, Object> values = new Dictionary<String, Object>();
private Object value;
private ElasticObject parent;
 
public ElasticObject() : this(null, null)
    {
    }
 
internal ElasticObject(ElasticObject parent, Object value)
    {
this.parent = parent;
this.value = (valueis ElasticObject) ? ((ElasticObject)value).value : value;
    }
 
public ElasticObject(Object value) : this(null, value)
    {
    }
 
publicoverride String ToString()
    {
if (this.value != null)
        {
return (this.value.ToString());
        }
else
        {
            var dict = thisas IDictionary<String, Object>;
return (String.Format("{{{0}}}", String.Join(", ", dict.Keys.Zip(dict.Values, (k, v) => String.Format("{0}={1}", k, v)))));
        }
    }
 
publicoverride Int32 GetHashCode()
    {
if (this.value != null)
        {
return (this.value.GetHashCode());
        }
else
        {
return (base.GetHashCode());
        }
    }
 
publicoverride Boolean Equals(Object obj)
    {
if (Object.ReferenceEquals(this, obj) == true)
        {
return (true);
        }
 
        var other = obj as ElasticObject;
 
if (other == null)
        {
return (false);
        }
 
if (Object.Equals(other.value, this.value) == false)
        {
return (false);
        }
 
return (this.values.SequenceEqual(other.values));
    }
 
publicoverride IEnumerable<String> GetDynamicMemberNames()
    {
return (this.values.Keys.Concat((this.value != null) ? TypeDescriptor.GetProperties(this.value).OfType<PropertyDescriptor>().Select(x => x.Name) : Enumerable.Empty<String>()));
    }
 
publicoverride Boolean TryBinaryOperation(BinaryOperationBinder binder, Object arg, out Object result)
    {
if (binder.Operation == ExpressionType.Equal)
        {
            result = Object.Equals(this.value, arg);
return (true);
        }
elseif (binder.Operation == ExpressionType.NotEqual)
        {
            result = !Object.Equals(this.value, arg);
return (true);
        }
 
return (base.TryBinaryOperation(binder, arg, out result));
    }
 
publicoverride Boolean TryUnaryOperation(UnaryOperationBinder binder, out Object result)
    {
if (binder.Operation == ExpressionType.Increment)
        {
if (this.valueis Int16)
            {
                result = (Int16)value + 1;
return (true);
            }
elseif (this.valueis Int32)
            {
                result = (Int32)value + 1;
return (true);
            }
elseif (this.valueis Int64)
            {
                result = (Int64)value + 1;
return (true);
            }
elseif (this.valueis UInt16)
            {
                result = (UInt16)value + 1;
return (true);
            }
elseif (this.valueis UInt32)
            {
                result = (UInt32)value + 1;
return (true);
            }
elseif (this.valueis UInt64)
            {
                result = (UInt64)value + 1;
return (true);
            }
elseif (this.valueis Decimal)
            {
                result = (Decimal)value + 1;
return (true);
            }
elseif (this.valueis Single)
            {
                result = (Single)value + 1;
return (true);
            }
elseif (this.valueis Double)
            {
                result = (Double)value + 1;
return (true);
            }
        }
elseif (binder.Operation == ExpressionType.Decrement)
        {
if (this.valueis Int16)
            {
                result = (Int16)value - 1;
return (true);
            }
elseif (this.valueis Int32)
            {
                result = (Int32)value - 1;
return (true);
            }
elseif (this.valueis Int64)
            {
                result = (Int64)value - 1;
return (true);
            }
elseif (this.valueis UInt16)
            {
                result = (UInt16)value - 1;
return (true);
            }
elseif (this.valueis UInt32)
            {
                result = (UInt32)value - 1;
return (true);
            }
elseif (this.valueis UInt64)
            {
                result = (UInt64)value - 1;
return (true);
            }
elseif (this.valueis Decimal)
            {
                result = (Decimal)value - 1;
return (true);
            }
elseif (this.valueis Single)
            {
                result = (Single)value - 1;
return (true);
            }
elseif (this.valueis Double)
            {
                result = (Double)value - 1;
return (true);
            }
        }
elseif (binder.Operation == ExpressionType.Not)
        {
if (this.valueis Boolean)
            {
                result = !(Boolean)value;
return (true);
            }
        }
elseif (binder.Operation == ExpressionType.OnesComplement)
        {
if (this.valueis Int16)
            {
                result = ~(Int16)value;
return (true);
            }
elseif (this.valueis Int32)
            {
                result = ~(Int32)value;
return (true);
            }
elseif (this.valueis Int64)
            {
                result = ~(Int64)value;
return (true);
            }
elseif (this.valueis UInt16)
            {
                result = ~(UInt16)value;
return (true);
            }
elseif (this.valueis UInt32)
            {
                result = ~(UInt32)value;
return (true);
            }
elseif (this.valueis UInt64)
            {
                result = ~(UInt64)value;
return (true);
            }
        }
 
returnbase.TryUnaryOperation(binder, out result);
    }
 
publicoverride Boolean TryInvokeMember(InvokeMemberBinder binder, Object[] args, out Object result)
    {
        var method = this.GetType().GetMethod(binder.Name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
 
if (method == null)
        {
foreach (var type inthis.GetType().GetInterfaces())
            {
                method = type.GetMethod(binder.Name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
 
if (method != null)
                {
break;
                }
            }
        }
 
if (method != null)
        {
            result = method.Invoke(this, args);
 
return (true);
        }
else
        {
return (base.TryInvokeMember(binder, args, out result));
        }
    }
 
publicoverride Boolean TryConvert(ConvertBinder binder, out Object result)
    {
if (this.value != null)
        {
if (binder.Type.IsInstanceOfType(this.value) == true)
            {
                result = this.value;
return (true);
            }
elseif (binder.Type.IsEnum == true)
            {
                result = Enum.Parse(binder.Type, this.value.ToString());
return (true);
            }
elseif ((typeof(IConvertible).IsAssignableFrom(binder.Type) == true) && (typeof(IConvertible).IsAssignableFrom(this.value.GetType()) == true))
            {
                result = Convert.ChangeType(this.value, binder.Type);
return (true);
            }
elseif (binder.Type == typeof(String))
            {
                result = this.value.ToString();
return (true);
            }
else
            {
                var converter = TypeDescriptor.GetConverter(binder.Type);
 
if (converter.CanConvertFrom(this.value.GetType()) == true)
                {
                    result = converter.ConvertFrom(this.value);
return (true);
                }
            }
        }
elseif (binder.Type.IsClass == true)
        {
            result = null;
return (true);
        }
 
        result = null;
return (false);
    }
 
publicoverride Boolean TrySetMember(SetMemberBinder binder, Object value)
    {
        (thisas IDictionary<String, Object>)[binder.Name] = value;
 
return (true);
    }
 
publicoverride Boolean TryGetMember(GetMemberBinder binder, out Object result)
    {
if (this.value != null)
        {
            var prop = TypeDescriptor.GetProperties(this.value)[binder.Name];
 
if (prop != null)
            {
                result = prop.GetValue(this.value);
return (true);
            }
        }
 
return (this.values.TryGetValue(binder.Name, out result));
    }
 
publicoverride Boolean TrySetIndex(SetIndexBinder binder, Object[] indexes, Object value)
    {
if ((indexes.Count() != 1) || (indexes.First() == null))
        {
return (false);
        }
 
        var key = indexes.First() as String;
 
if (indexes.First() is Int32)
        {
            var index = (Int32)indexes.First();
 
if (this.values.Count < index)
            {
                key = this.values.ElementAt(index).Key;
            }
        }
elseif (key == null)
        {
return (false);
        }
 
        (thisas IDictionary<String, Object>)[key] = value;
 
return (true);
    }
 
publicoverride Boolean TryGetIndex(GetIndexBinder binder, Object[] indexes, out Object result)
    {
if ((indexes.Count() != 1) || (indexes.First() == null))
        {
            result = null;
return (false);
        }
 
        var key = indexes.First() as String;
 
if (key != null)
        {
if (this.value != null)
            {
                var prop = TypeDescriptor.GetProperties(this.value)[key];
 
if (prop != null)
                {
                    result = prop.GetValue(this.value);
return (true);
                }
            }
 
if (String.Equals("$parent", key, StringComparison.InvariantCultureIgnoreCase) == true)
            {
                result = this.parent;
return (true);
            }
elseif (String.Equals("$value", key, StringComparison.InvariantCultureIgnoreCase) == true)
            {
                result = this.value;
return (true);
            }
elseif (String.Equals("$type", key, StringComparison.InvariantCultureIgnoreCase) == true)
            {
                result = ((this.value != null) ? this.value.GetType() : null);
return (true);
            }
elseif (String.Equals("$root", key, StringComparison.InvariantCultureIgnoreCase) == true)
            {
                var root = this;
 
while (root != null)
                {
if (root.parent == null)
                    {
break;
                    }
 
                    root = root.parent;
                }
 
                result = root;
return (true);
            }
elseif (String.Equals("$path", key, StringComparison.InvariantCultureIgnoreCase) == true)
            {
                var list = new LinkedList<string>();
 
                var p = this.parent;
                var previous = (Object)this;
 
while (p != null)
                {
                    var kv = p.values.SingleOrDefault(x => (Object)x.Value == (Object)previous);
 
                    list.AddFirst(kv.Key);
 
                    previous = ((ElasticObject)kv.Value).parent;
                    p = p.parent;
                }
 
                result = String.Join(".", list);
return (true);
            }
else
            {
return (this.values.TryGetValue(key, out result));
            }
        }
elseif (indexes.First() is Int32)
        {
            var index = (Int32)indexes.First();
 
if (this.values.Count < index)
            {
                result = this.values.ElementAt(index).Value;
return (true);
            }
        }
 
        result = null;
return (false);
    }
 
void IDictionary<String,Object>.Add(String key, Object value)
    {
        (thisas IDictionary<String,Object>)[key] = value;
    }
 
    Boolean IDictionary<String,Object>.ContainsKey(String key)
    {
return (this.GetDynamicMemberNames().Contains(key));
    }
 
    ICollection<String> IDictionary<String,Object>.Keys
    {
        get
        {
return (this.GetDynamicMemberNames().ToList());
        }
    }
 
    Boolean IDictionary<String,Object>.Remove(String key)
    {
return (this.values.Remove(key));
    }
 
    Boolean IDictionary<String,Object>.TryGetValue(String key, out Object value)
    {
if (this.value != null)
        {
            var prop = TypeDescriptor.GetProperties(this.value)[key];
 
if (prop != null)
            {
value = prop.GetValue(this.value);
return (true);
            }
        }
 
return (this.values.TryGetValue(key, outvalue));
    }
 
    ICollection<Object> IDictionary<String,Object>.Values
    {
        get
        {
return (this.values.Values.Concat((this.value != null) ? TypeDescriptor.GetProperties(this.value).OfType<PropertyDescriptor>().Select(x => x.GetValue(this.value)) : Enumerable.Empty<Object>()).ToList());
        }
    }
 
    Object IDictionary<String,Object>.this[String key]
    {
        get
        {
if (this.value != null)
            {
                var prop = TypeDescriptor.GetProperties(this.value)[key];
 
if (prop != null)
                {
return (prop.GetValue(this.value));
                }
            }
 
return (this.values[key]);
        }
        set
        {
if (valueis ElasticObject)
            {
this.values[key] = value;
                ((ElasticObject) value).parent = this;
            }
elseif (value == null)
            {
this.values[key] = null;
            }
else
            {
this.values[key] = new ElasticObject(this, value);
            }
 
this.OnPropertyChanged(new PropertyChangedEventArgs(key));
        }
    }
 
privatevoid OnPropertyChanged(PropertyChangedEventArgs e)
    {
        var handler = this.PropertyChanged;
 
if (handler != null)
        {
            handler(this, e);
        }
    }
 
void ICollection<KeyValuePair<String, Object>>.Add(KeyValuePair<String, Object> item)
    {
        (thisas IDictionary<String, Object>)[item.Key] = item.Value;
    }
 
void ICollection<KeyValuePair<String, Object>>.Clear()
    {
this.values.Clear();
    }
 
    Boolean ICollection<KeyValuePair<String, Object>>.Contains(KeyValuePair<String, Object> item)
    {
return (this.values.Contains(item));
    }
 
void ICollection<KeyValuePair<String, Object>>.CopyTo(KeyValuePair<String, Object>[] array, Int32 arrayIndex)
    {
this.values.CopyTo(array, arrayIndex);
    }
 
    Int32 ICollection<KeyValuePair<String, Object>>.Count
    {
        get
        {
return (this.values.Count);
        }
    }
 
    Boolean ICollection<KeyValuePair<String,Object>>.IsReadOnly
    {
        get
        {
return (this.values.IsReadOnly);
        }
    }
 
    Boolean ICollection<KeyValuePair<String,Object>>.Remove(KeyValuePair<String, Object> item)
    {
return (this.values.Remove(item));
    }
 
    IEnumerator<KeyValuePair<String, Object>> IEnumerable<KeyValuePair<String,Object>>.GetEnumerator()
    {
return (this.values.GetEnumerator());
    }
 
    IEnumerator IEnumerable.GetEnumerator()
    {
return ((thisas IDictionary<String, Object>).GetEnumerator());
    }
 
publicevent PropertyChangedEventHandler PropertyChanged;
 
    Object ICloneable.Clone()
    {
        var clone = new ElasticObject(null, this.value) as IDictionary<String, Object>;
 
foreach (var key inthis.values.Keys)
        {
            clone[key] = (this.values[key] is ICloneable) ? (this.values[key] as ICloneable).Clone() : this.values[key];
        }
 
return (clone);
    }
}

Granted, there may be some inefficiencies, I leave it to you, dear reader, the task to spot them and letting me know!

Now, the  TypeDescriptionProvider classes:

[Serializable]
publicsealedclass ElasticObjectTypeDescriptionProvider : TypeDescriptionProvider
{
publicoverride ICustomTypeDescriptor GetTypeDescriptor(Type objectType, Object instance)
    {
return (new ElasticObjectTypeDescriptor(instance));
    }
}
 
[Serializable]
publicsealedclass ElasticObjectTypeDescriptor : CustomTypeDescriptor
{
privatereadonly ElasticObject instance;
 
public ElasticObjectTypeDescriptor(Object instance)
    {
this.instance = instance as ElasticObject;
    }
 
publicoverride PropertyDescriptorCollection GetProperties()
    {
if (this.instance != null)
        {
returnnew PropertyDescriptorCollection((this.instance as IDictionary<String, Object>).Keys.Select(x => new ElasticObjectPropertyDescriptor(x)).ToArray());
        }
else
        {
return (base.GetProperties());
        }
    }
 
publicoverride TypeConverter GetConverter()
    {
return (new ElasticObjectTypeConverter());
    }
 
publicoverride AttributeCollection GetAttributes()
    {
return (new AttributeCollection(new SerializableAttribute()));
    }
}

And a custom converter:

[Serializable]
publicsealedclass ElasticObjectTypeConverter : TypeConverter
{
publicoverride Boolean CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
return (true);
    }
 
publicoverride Boolean CanConvertTo(ITypeDescriptorContext context, Type destinationType)
    {
if (destinationType == typeof(ElasticObject))
        {
return (true);
        }
else
        {
return (base.CanConvertTo(context, destinationType));
        }
    }
 
publicoverride Object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, Object value)
    {
if (valueis ElasticObject)
        {
return (value);
        }
else
        {
return (new ElasticObject(null, value));
        }
    }
 
publicoverride Object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, Object value, Type destinationType)
    {
if (destinationType == typeof(ElasticObject))
        {
return (this.ConvertFrom(context, culture, value));
        }
else
        {
returnbase.ConvertTo(context, culture, value, destinationType);
        }
    }
 
publicoverride PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, Object value, Attribute[] attributes)
    {
        var provider = TypeDescriptor.GetProvider(value) as TypeDescriptionProvider;
        var descriptor = provider.GetTypeDescriptor(value);
return (descriptor.GetProperties(attributes));
    }
 
publicoverride Boolean GetPropertiesSupported(ITypeDescriptorContext context)
    {
return (true);
    }
 
publicoverride Object CreateInstance(ITypeDescriptorContext context, IDictionary propertyValues)
    {
        dynamic obj = new ElasticObject();
 
foreach (var key in propertyValues.Keys)
        {
            obj[key.ToString()] = propertyValues[key];
        }
 
return (obj);
    }
 
publicoverride Boolean GetCreateInstanceSupported(ITypeDescriptorContext context)
    {
return (true);
    }
 
publicoverride Boolean GetStandardValuesSupported(ITypeDescriptorContext context)
    {
return (false);
    }
}

That’s about it. Have fun and make sure you send me your thoughts!



Viewing all articles
Browse latest Browse all 404

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>