2026-01-15

[C#] 別再手寫 Select - Facet.Net 扁平化 Model 的優點與實務限制

最近看到一個小眾的套件有點意思,就測試了一下 叫做 Facet.Net ,這套件有一些方便的地方

但是也有一些限制,這邊就簡單介紹一下,我覺得在製作一些 API  的時候會是有用且方便的..


這套件主要是在用於 "扁平化" 你的資料模型,當你的模型是巢狀的時候他會進行把巢狀的物件拉到第一層來

直接看案例,這邊是我原本的模型

public class Order { public int Id { get; set; } public Customer Customer { get; set; } = default!; public List<orderline> Lines { get; set; } = new(); public DateTime CreatedAt { get; set; } } public class Customer { public int Id { get; set; } public string Name { get; set; } = ""; public Address Address { get; set; } = default!; } public class Address { public string City { get; set; } = ""; public string Country { get; set; } = ""; } public class OrderLine { public string ProductName { get; set; } public decimal Price { get; set; } public int Quantity { get; set; } } </orderline>


這邊我先給出,將這資料序列畫成 JSON 之後的長相

[ { "Id": 1, "Customer": { "Id": 1, "Name": "許當麻", "Address": { "City": "台北市", "Country": "台灣" } }, "Lines": [ { "ProductName": "筆記型電腦", "Price": 28000, "Quantity": 1 }, { "ProductName": "無線滑鼠", "Price": 1200, "Quantity": 1 } ], "CreatedAt": "2026-01-14T11:59:31.505265+08:00" }, { "Id": 2, "Customer": { "Id": 2, "Name": "林怡君", "Address": { "City": "新北市", "Country": "台灣" } }, "Lines": [ { "ProductName": "27吋螢幕", "Price": 9500, "Quantity": 1 } ], "CreatedAt": "2026-01-13T11:59:31.5133105+08:00" } ]


之後我們只需要建立一個物件,上面加上 Attribute 就可以,之後在 LINQ 下進行 .Select 輸出

[Flatten(typeof(Order))] public partial class OrderFlatten { }

測試轉換結果

var flattenData = FakeOrderData.FakeData1 .AsQueryable() .Where(x =&gt; x.Id &lt;= 2) .Select(OrderFlatten.Projection) .ToList(); Console.WriteLine(JsonConvert.SerializeObject(flattenData));

輸出結果

[ { "Id": 1, "CustomerId": 1, "CustomerName": "許當麻", "CustomerAddressCity": "台北市", "CustomerAddressCountry": "台灣", "CreatedAt": "2026-01-14T11:59:31.505265+08:00" }, { "Id": 2, "CustomerId": 2, "CustomerName": "林怡君", "CustomerAddressCity": "新北市", "CustomerAddressCountry": "台灣", "CreatedAt": "2026-01-13T11:59:31.5133105+08:00" } ]


這樣是不是就非常簡單的將 Customer=> Name , AdddressCity ..等都往上拉了一個層級,甚至你不用寫轉換的程式碼

再來他還有一些其他常見的用法,如果你在進行關聯你可以透過調整 Arrtibute 把子項目的 Id 不要顯示出來

[Flatten(typeof(Order), IgnoreNestedIds = true)] public partial class OrderFlattenIgonreIds { } //使用方法 var flattenIgonreNestedIdsData = FakeOrderData.FakeData1 .AsQueryable() .Where(x =&gt; x.Id &lt;= 2) .Select(OrderFlattenIgonreIds.Projection) .ToList(); Console.WriteLine(JsonConvert.SerializeObject(flattenIgonreNestedIdsData));

輸出結果

[ { "Id": 1, "CustomerName": "許當麻", "CustomerAddressCity": "台北市", "CustomerAddressCountry": "台灣", "CreatedAt": "2026-01-14T11:59:31.505265+08:00" }, { "Id": 2, "CustomerName": "林怡君", "CustomerAddressCity": "新北市", "CustomerAddressCountry": "台灣", "CreatedAt": "2026-01-13T11:59:31.5133105+08:00" } ]

不過,如果如果你是要比較複雜要多一個 多出來的屬性,還是得要自己作 mapping 自己寫一個 Projection

[Flatten(typeof(Order))] public partial class OrderFlattenWithTotalAmount { public decimal TotalAmount { get; set; } public static Expression<func orderflattenwithtotalamount="" rder="">&gt; WithTotalAmount =&gt; o =&gt; new OrderFlattenWithTotalAmount { // Flatten 對應的基本欄位&#65288;你要&#12300;照名填&#12301;&#65289; Id = o.Id, CustomerId = o.Customer.Id, CustomerName = o.Customer.Name, CustomerAddressCity = o.Customer.Address.City, CustomerAddressCountry = o.Customer.Address.Country, CreatedAt = o.CreatedAt, TotalAmount = o.Lines.Sum(l =&gt; l.Price * l.Quantity) }; } //使用方法 var customerData = FakeOrderData.FakeData1 .AsQueryable() .Where(x =&gt; x.Id &lt;= 2) .Select(OrderFlattenWithTotalAmount.WithTotalAmount) .ToList(); Console.WriteLine(JsonConvert.SerializeObject(customerData)); </func>

輸出結果

[ { "TotalAmount": 29200, "Id": 1, "CustomerId": 1, "CustomerName": "許當麻", "CustomerAddressCity": "台北市", "CustomerAddressCountry": "台灣", "CreatedAt": "2026-01-14T11:59:31.505265+08:00" }, { "TotalAmount": 9500, "Id": 2, "CustomerId": 2, "CustomerName": "林怡君", "CustomerAddressCity": "新北市", "CustomerAddressCountry": "台灣", "CreatedAt": "2026-01-13T11:59:31.5133105+08:00" } ]

做個結論,Facet.Net 解決的是"結構型轉換",而不是"查詢邏輯" 這兩件事一開始就該分開看,在快速處理一些 API 輸出時,你有扁平化的需求,他是一個好東西,但是他遇到是 List<object> ,或是陣列型的屬性時

他會略過不處理,所以在一些 "圖方便" 的狀況他是一個不錯的選擇,但是如果要需要客制化還是得寫 expreesion ,你說他雞肋嗎

倒也不至於,只是在一些狀況下的確可以快速產生一個需要的物件去輸出,但是現實雖然常常都是比較複雜的..



--

The bug existed in all possible states. Until I ran the code.

如果這篇文章有幫助到您幫我分享一下,讓我有寫下去的動力...