這個做法在說明如何使用 DynamicObjectIDynamicMetaObjectProvider 做到在執行時期建立出一個新的型別。

只要繼承 DynamicObject 類別就能建造出一個擁有動態能力的型別,關鍵就是依靠 IDynamicMetaObjectProvider 提供的 GetMetaObject 方法 來達成的,因為 DynamicObject 已經有實做 IDynamicMetaObjectProvider 介面所以只需要繼承 DynamicObject 就好。

接下來我們需要在衍生類別覆寫 TryGetMemberTrySetMember 來自定義存取行為,可以透過建立一個字典存放 keyvalue 來模擬屬性存取的效果。

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 等需要額外解析資料的場景。