Elastic Object Implementation in .NET
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))]
public sealed class ElasticObject : DynamicObject, IDictionary<String, Object>, ICloneable, INotifyPropertyChanged
{
private static readonly String [] SpecialKeys = new String[] { "$Path", "$Parent", "$Root", "$Value", "$Type" };
private readonly 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 = (value is ElasticObject) ? ((ElasticObject)value).value : value;
}
public ElasticObject(Object value) : this(null, value)
{
}
public override String ToString()
{
if (this.value != null)
{
return (this.value.ToString());
}
else
{
var dict = this as IDictionary<String, Object>;
return (String.Format("{{{0}}}", String.Join(", ", dict.Keys.Zip(dict.Values, (k, v) => String.Format("{0}={1}", k, v)))));
}
}
public override Int32 GetHashCode()
{
if (this.value != null)
{
return (this.value.GetHashCode());
}
else
{
return (base.GetHashCode());
}
}
public override 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));
}
public override IEnumerable<String> GetDynamicMemberNames()
{
return (this.values.Keys.Concat((this.value != null) ? TypeDescriptor.GetProperties(this.value).OfType<PropertyDescriptor>().Select(x => x.Name) : Enumerable.Empty<String>()));
}
public override Boolean TryBinaryOperation(BinaryOperationBinder binder, Object arg, out Object result)
{
if (binder.Operation == ExpressionType.Equal)
{
result = Object.Equals(this.value, arg);
return (true);
}
else if (binder.Operation == ExpressionType.NotEqual)
{
result = !Object.Equals(this.value, arg);
return (true);
}
return (base.TryBinaryOperation(binder, arg, out result));
}
public override Boolean TryUnaryOperation(UnaryOperationBinder binder, out Object result)
{
if (binder.Operation == ExpressionType.Increment)
{
if (this.value is Int16)
{
result = (Int16)value + 1;
return (true);
}
else if (this.value is Int32)
{
result = (Int32)value + 1;
return (true);
}
else if (this.value is Int64)
{
result = (Int64)value + 1;
return (true);
}
else if (this.value is UInt16)
{
result = (UInt16)value + 1;
return (true);
}
else if (this.value is UInt32)
{
result = (UInt32)value + 1;
return (true);
}
else if (this.value is UInt64)
{
result = (UInt64)value + 1;
return (true);
}
else if (this.value is Decimal)
{
result = (Decimal)value + 1;
return (true);
}
else if (this.value is Single)
{
result = (Single)value + 1;
return (true);
}
else if (this.value is Double)
{
result = (Double)value + 1;
return (true);
}
}
else if (binder.Operation == ExpressionType.Decrement)
{
if (this.value is Int16)
{
result = (Int16)value - 1;
return (true);
}
else if (this.value is Int32)
{
result = (Int32)value - 1;
return (true);
}
else if (this.value is Int64)
{
result = (Int64)value - 1;
return (true);
}
else if (this.value is UInt16)
{
result = (UInt16)value - 1;
return (true);
}
else if (this.value is UInt32)
{
result = (UInt32)value - 1;
return (true);
}
else if (this.value is UInt64)
{
result = (UInt64)value - 1;
return (true);
}
else if (this.value is Decimal)
{
result = (Decimal)value - 1;
return (true);
}
else if (this.value is Single)
{
result = (Single)value - 1;
return (true);
}
else if (this.value is Double)
{
result = (Double)value - 1;
return (true);
}
}
else if (binder.Operation == ExpressionType.Not)
{
if (this.value is Boolean)
{
result = !(Boolean)value;
return (true);
}
}
else if (binder.Operation == ExpressionType.OnesComplement)
{
if (this.value is Int16)
{
result = ~(Int16)value;
return (true);
}
else if (this.value is Int32)
{
result = ~(Int32)value;
return (true);
}
else if (this.value is Int64)
{
result = ~(Int64)value;
return (true);
}
else if (this.value is UInt16)
{
result = ~(UInt16)value;
return (true);
}
else if (this.value is UInt32)
{
result = ~(UInt32)value;
return (true);
}
else if (this.value is UInt64)
{
result = ~(UInt64)value;
return (true);
}
}
return base.TryUnaryOperation(binder, out result);
}
public override 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 in this.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));
}
}
public override Boolean TryConvert(ConvertBinder binder, out Object result)
{
if (this.value != null)
{
if (binder.Type.IsInstanceOfType(this.value) == true)
{
result = this.value;
return (true);
}
else if (binder.Type.IsEnum == true)
{
result = Enum.Parse(binder.Type, this.value.ToString());
return (true);
}
else if ((typeof(IConvertible).IsAssignableFrom(binder.Type) == true) && (typeof(IConvertible).IsAssignableFrom(this.value.GetType()) == true))
{
result = Convert.ChangeType(this.value, binder.Type);
return (true);
}
else if (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);
}
}
}
else if (binder.Type.IsClass == true)
{
result = null;
return (true);
}
result = null;
return (false);
}
public override Boolean TrySetMember(SetMemberBinder binder, Object value)
{
(this as IDictionary<String, Object>)[binder.Name] = value;
return (true);
}
public override 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));
}
public override 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;
}
}
else if (key == null)
{
return (false);
}
(this as IDictionary<String, Object>)[key] = value;
return (true);
}
public override 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);
}
else if (String.Equals("$value", key, StringComparison.InvariantCultureIgnoreCase) == true)
{
result = this.value;
return (true);
}
else if (String.Equals("$type", key, StringComparison.InvariantCultureIgnoreCase) == true)
{
result = ((this.value != null) ? this.value.GetType() : null);
return (true);
}
else if (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);
}
else if (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));
}
}
else if (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)
{
(this as 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, out value));
}
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 (value is ElasticObject)
{
this.values[key] = value;
((ElasticObject) value).parent = this;
}
else if (value == null)
{
this.values[key] = null;
}
else
{
this.values[key] = new ElasticObject(this, value);
}
this.OnPropertyChanged(new PropertyChangedEventArgs(key));
}
}
private void OnPropertyChanged(PropertyChangedEventArgs e)
{
var handler = this.PropertyChanged;
if (handler != null)
{
handler(this, e);
}
}
void ICollection<KeyValuePair<String, Object>>.Add(KeyValuePair<String, Object> item)
{
(this as 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 ((this as IDictionary<String, Object>).GetEnumerator());
}
public event PropertyChangedEventHandler PropertyChanged;
Object ICloneable.Clone()
{
var clone = new ElasticObject(null, this.value) as IDictionary<String, Object>;
foreach (var key in this.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]
public sealed class ElasticObjectTypeDescriptionProvider : TypeDescriptionProvider
{
public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, Object instance)
{
return (new ElasticObjectTypeDescriptor(instance));
}
}
[Serializable]
public sealed class ElasticObjectTypeDescriptor : CustomTypeDescriptor
{
private readonly ElasticObject instance;
public ElasticObjectTypeDescriptor(Object instance)
{
this.instance = instance as ElasticObject;
}
public override PropertyDescriptorCollection GetProperties()
{
if (this.instance != null)
{
return new PropertyDescriptorCollection((this.instance as IDictionary<String, Object>).Keys.Select(x => new ElasticObjectPropertyDescriptor(x)).ToArray());
}
else
{
return (base.GetProperties());
}
}
public override TypeConverter GetConverter()
{
return (new ElasticObjectTypeConverter());
}
public override AttributeCollection GetAttributes()
{
return (new AttributeCollection(new SerializableAttribute()));
}
}
And a custom converter:
[Serializable]
public sealed class ElasticObjectTypeConverter : TypeConverter
{
public override Boolean CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return (true);
}
public override Boolean CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(ElasticObject))
{
return (true);
}
else
{
return (base.CanConvertTo(context, destinationType));
}
}
public override Object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, Object value)
{
if (value is ElasticObject)
{
return (value);
}
else
{
return (new ElasticObject(null, value));
}
}
public override Object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, Object value, Type destinationType)
{
if (destinationType == typeof(ElasticObject))
{
return (this.ConvertFrom(context, culture, value));
}
else
{
return base.ConvertTo(context, culture, value, destinationType);
}
}
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, Object value, Attribute[] attributes)
{
var provider = TypeDescriptor.GetProvider(value) as TypeDescriptionProvider;
var descriptor = provider.GetTypeDescriptor(value);
return (descriptor.GetProperties(attributes));
}
public override Boolean GetPropertiesSupported(ITypeDescriptorContext context)
{
return (true);
}
public override Object CreateInstance(ITypeDescriptorContext context, IDictionary propertyValues)
{
dynamic obj = new ElasticObject();
foreach (var key in propertyValues.Keys)
{
obj[key.ToString()] = propertyValues[key];
}
return (obj);
}
public override Boolean GetCreateInstanceSupported(ITypeDescriptorContext context)
{
return (true);
}
public override Boolean GetStandardValuesSupported(ITypeDescriptorContext context)
{
return (false);
}
}
That’s about it. Have fun and make sure you send me your thoughts!