這套件主要是在用於 "扁平化" 你的資料模型,當你的模型是巢狀的時候他會進行把巢狀的物件拉到第一層來
直接看案例,這邊是我原本的模型
public class Order
{
public int Id { get; set; }
public Customer Customer { get; set; } = default!;
public List 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; }
}
這邊我先給出,將這資料序列畫成 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 => x.Id <= 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 => x.Id <= 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> WithTotalAmount =>
o => new OrderFlattenWithTotalAmount
{
// Flatten 對應的基本欄位(你要「照名填」)
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 => l.Price * l.Quantity)
};
}
//使用方法
var customerData = FakeOrderData.FakeData1
.AsQueryable()
.Where(x => x.Id <= 2)
.Select(OrderFlattenWithTotalAmount.WithTotalAmount)
.ToList();
Console.WriteLine(JsonConvert.SerializeObject(customerData));
輸出結果
[
{
"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"
}
]
做個結論,在快速處理一些 API 輸出時,你有扁平化的需求,他是一個好東西,但是他遇到是 List<object> ,或是陣列型的屬性時
他會略過不處理,所以在一些 "圖方便" 的狀況他是一個不錯的選擇,但是如果要需要客制化還是得寫 expreesion ,你說他雞肋嗎
倒也不至於,只是在一些狀況下的確可以快速產生一個需要的物件去輸出,但是現實雖然常常都是比較複雜的..
--
本文原文首發於個人部落格:[C#] 別再手寫 Select - Facet.Net 扁平化 Model 的優點與實務限制
--
---
The bug existed in all possible states.
Until I ran the code.