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

  • 130
  • 0

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

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

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

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


  
  
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.