C# 動態物件類型 ExpandoObject 介紹

ExpandoObject 能夠讓我們在執行時動態增加和移除物件屬性、方法,就像是增強版的 Dictionary (ExpandoObject 本身有實作 IDictionary 介面),在處理動態資料時非常好用。

建立物件

建立 ExpandoObject 需要使用 dynamic 關鍵字宣告
    
dynamic obj = new ExpandoObject();
    

增加屬性

在物件後面加上點 (.) 後可以直接建立並使用動態屬性,例如 obj.A 就是直接使用 A 屬性,會自動判斷型別,不過都需要先賦值才能使用
    
obj.PropertyInt = 123;
Console.WriteLine(obj.PropertyInt); // 123
Console.WriteLine(obj.PropertyInt.GetType()); // System.Int32

obj.PropertyString = "PropertyString";
Console.WriteLine(obj.PropertyString); // PropertyString
Console.WriteLine(obj.PropertyString.GetType()); // System.String

obj.PropertyDateTime = DateTime.Now;
Console.WriteLine(obj.PropertyDateTime); // 2022/12/16 下午 23:59:59
Console.WriteLine(obj.PropertyDateTime.GetType()); // System.DateTime

obj.PropertyDouble = 123.456;
Console.WriteLine(obj.PropertyDouble); // 123.456
Console.WriteLine(obj.PropertyDouble.GetType()); // System.Double
    

如果還沒有賦值就呼叫會出現下面的錯誤:
    
Console.WriteLine(obj.Property2); // 拋出錯誤

/*
Unhandled exception. Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'System.Dynamic.ExpandoObject' does not contain a definition for 'Property2'
   at CallSite.Target(Closure , CallSite , Object )
   at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0)
   at Program.<Main>$(String[] args) in C:\Users\ruyut\Documents\RiderProjects\test\2022\ConsoleAppDotNet6Test\ConsoleAppDotNet6Test\Program.cs:line 27
*/
    

註:呼叫不存在的屬性在編譯時並不會出現錯誤,直到執行時才會拋出錯誤

動態使用屬性

上面使用的都需要和一般使用屬性的方式一樣,使用點 (.) 再加上屬性名稱,不過這樣就無法動態的呼叫了。

還好 ExpandoObject 有實作 IDictionary<String, Object> 介面,所以我們可以像是使用 Dictionary 一樣的方式使用 ExpandoObject 來取出或放入屬性,兩者也可以交叉使用,非常靈活
    
dynamic obj = new ExpandoObject();

var dictionary = obj as IDictionary<String, Object>;
dictionary["Name"] = "Ruyut";
dictionary.Add("Id", 1);

Console.WriteLine(obj.Name); // Ruyut
Console.WriteLine(obj.Id); // 1

obj.Id = 2;
Console.WriteLine(dictionary["Id"]); // 2
    

列舉出所有屬性

剛剛提到過有實作 IDictionary ,所以要查看所有屬性和內容非常的容易:
    
foreach (var item in obj as IDictionary<String, Object>)
{
    Console.WriteLine($"{item.Key} : {item.Value}");
}

// Name : Ruyut
// Id : 2
    

刪除屬性

刪除屬性需要先將 dynamic 轉換為 IDictionary ,再使用 Remove 方法移除
    
var dictionary = obj as IDictionary<String, Object>;

dictionary.Remove("Id");
    

屬性變更通知

因為 ExpandoObject 有實作 INotifyPropertyChanged 介面,我們可以使用下面的方式將有變更的屬性和新的內容顯示出來
    
dynamic obj = new ExpandoObject();

var dictionary = obj as IDictionary<String, Object>;

((INotifyPropertyChanged)obj).PropertyChanged += (sender, e) =>
{
    string notify = $"{e.PropertyName} changed";
    if (dictionary.ContainsKey(e.PropertyName)) // 檢查屬性沒有被移除
    {
        notify += $", New value: {dictionary[e.PropertyName]}";
    }

    Console.WriteLine(notify);
};
    

動態增加方法

這裡直接示範使用委派(Delegate)建立沒有回傳值和有回傳值的方法
    
// 顯示訊息 (沒有回傳值)
obj.SayHello = new Action<string>(name => Console.WriteLine($"Hello {name}!"));
obj.SayHello("Ruyut");


// 計算 A + B (有回傳值)
obj.Add = new Func<int, int, int>((a, b) => a + b);
Console.WriteLine(obj.Add(1, 2));
    


參考資料:
Microsoft.Learn - ExpandoObject Class
Microsoft.Learn - Action<T> Delegate
Microsoft.Learn - Func<T,TResult> Delegate
Microsoft.Learn - INotifyPropertyChanged Interface
Microsoft.Learn - IDictionary<TKey,TValue> Interface

留言