這個做法在說明如何使用 DynamicObject
與 IDynamicMetaObjectProvider
做到在執行時期建立出一個新的型別。
只要繼承 DynamicObject
類別就能建造出一個擁有動態能力的型別,關鍵就是依靠 IDynamicMetaObjectProvider
提供的 GetMetaObject 方法
來達成的,因為 DynamicObject
已經有實做 IDynamicMetaObjectProvider
介面所以只需要繼承 DynamicObject
就好。
接下來我們需要在衍生類別覆寫 TryGetMember
、TrySetMember
來自定義存取行為,可以透過建立一個字典存放 key
與 value
來模擬屬性存取的效果。
void Main()
{
dynamic dynamicProperties = new DynamicPropertyBag();
dynamicProperties.Name = "Allen"; // 動態新增屬性 Name
dynamicProperties.Age = 30; // 動態新增屬性 Age
Console.WriteLine(dynamicProperties.Name);
Console.WriteLine(dynamicProperties);
}
public class DynamicPropertyBag : DynamicObject
{
private Dictionary<string, object> storage = new Dictionary<string, object>();
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
if (storage.ContainsKey(binder.Name))
{
result = storage[binder.Name];
return true;
}
result = null;
return false;
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
storage[binder.Name] = value; // Add or update property
return true;
}
public override string ToString()
{
StringWriter writer = new StringWriter();
foreach (var entry in storage)
writer.WriteLine($"{entry.Key}:\t{entry.Value}");
return writer.ToString();
}
}
這個特性就很適合處理動態資料解析,例如回傳一個 XML 格式的資料就可以建立一個特殊處理的 DynamicObject
來存取 XML 其中一個元素。
void Main()
{
var xml = XElement.Parse("<Planets><Planet><Name>Earth</Name></Planet></Planets>");
dynamic dynamicXML = new DynamicXElement(xml);
Console.WriteLine(dynamicXML.Planet.Name); // Earth
}
public class DynamicXElement : DynamicObject
{
private readonly XElement xmlSource;
public DynamicXElement(XElement source)
{
xmlSource = source;
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
if (binder.Name == "Value")
{
result = xmlSource?.Value ?? "";
return true;
}
result = xmlSource?.Element(binder.Name) != null
? new DynamicXElement(xmlSource.Element(binder.Name))
: new DynamicXElement(null);
return true;
}
public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
{
if (indexes.Length == 2 && indexes[0] is string && indexes[1] is int)
{
var nodes = xmlSource.Elements((string)indexes[0]);
result = new DynamicXElement(nodes.ElementAtOrDefault((int)indexes[1]));
return true;
}
result = new DynamicXElement(null);
return false;
}
public override string ToString() => xmlSource?.ToString() ?? string.Empty;
}
如果沒辦法繼承 DynamicObject
的話可以自行實做 IDynamicMetaObjectProvider
介面,下面是它的原始碼。
#region IDynamicMetaObjectProvider Members
/// <summary>
/// Returns the <see cref="DynamicMetaObject" /> responsible for binding operations performed on this object,
/// using the virtual methods provided by this class.
/// </summary>
/// <param name="parameter">The expression tree representation of the runtime value.</param>
/// <returns>
/// The <see cref="DynamicMetaObject" /> to bind this object. The object can be encapsulated inside of another
/// <see cref="DynamicMetaObject"/> to provide custom behavior for individual actions.
/// </returns>
public virtual DynamicMetaObject GetMetaObject(Expression parameter) => new MetaDynamic(parameter, this);
#endregion
接下來就新增 DynamicDictionaryMetaObject
物件負責解析成員,要特別注意每次呼叫的時候一定會執行 GetMetaObject
所以在撰寫
程式碼的時候必須考慮程式運作效率。
void Main()
{
dynamic dynamicProperties = new DynamicDictionary2();
dynamicProperties.Name = "Allen"; // 動態新增屬性 Name
dynamicProperties.Age = 30; // 動態新增屬性 Age
Console.WriteLine(dynamicProperties.Name);
Console.WriteLine(dynamicProperties);
}
public class DynamicDictionary2 : IDynamicMetaObjectProvider
{
DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(Expression parameter)
{
return new DynamicDictionaryMetaObject(parameter, this);
}
private Dictionary<string, object> storage = new Dictionary<string, object>();
public object SetDictionaryEntry(string key, object value)
{
if (storage.ContainsKey(key))
storage[key] = value;
else
storage.Add(key, value);
return value;
}
public object GetDictionaryEntry(string key)
{
object result = null;
if (storage.ContainsKey(key))
{
result = storage[key];
}
return result;
}
public override string ToString()
{
StringWriter message = new StringWriter();
foreach (var item in storage)
message.WriteLine($"{item.Key}:\t{item.Value}");
return message.ToString();
}
}
public class DynamicDictionaryMetaObject : DynamicMetaObject
{
public DynamicDictionaryMetaObject(Expression expression, DynamicDictionary2 value)
: base(expression, BindingRestrictions.Empty, value) { }
public override DynamicMetaObject BindSetMember(SetMemberBinder binder, DynamicMetaObject value)
{
var method = typeof(DynamicDictionary2).GetMethod(nameof(DynamicDictionary2.SetDictionaryEntry));
var arguments = new Expression[]
{
Expression.Constant(binder.Name),
Expression.Convert(value.Expression, typeof(object))
};
var methodCall = Expression.Call(Expression.Convert(Expression, LimitType), method, arguments);
return new DynamicMetaObject(methodCall, BindingRestrictions.GetTypeRestriction(Expression, LimitType));
}
public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
{
var method = typeof(DynamicDictionary2).GetMethod(nameof(DynamicDictionary2.GetDictionaryEntry));
var arguments = new[] { Expression.Constant(binder.Name) };
var methodCall = Expression.Call(Expression.Convert(Expression, LimitType), method, arguments);
return new DynamicMetaObject(methodCall, BindingRestrictions.GetTypeRestriction(Expression, LimitType));
}
}
Summary
這個做法建議優先使用 DynamicObject
,如果沒辦法才自行實做 IDynamicMetaObjectProvider
,但自行實做要保證程式碼沒有進行多餘的計算,
不然會影響程式執行性能,這個做法很適合處理動態的資料源例如 XML 與 JSON 等需要額外解析資料的場景。