OData WebAPI實踐-OData與EDM

2023-05-11 15:00:17

本文屬於 OData 系列

引言

在 OData 中,EDM(Entity Data Model) 代表「實體資料模型」,它是一種用於表示 Web API 中的結構化資料的格式。EDM 定義了可以由 OData 服務公開的資料型別、實體和關係。 EDM 也提供了一些規則來描述資料模型中的實體之間的關係,例如繼承、關聯和複合型別。EDM 是 OData 協定的核心組成部分之一,它允許使用者端和伺服器之間以一致的方式交換和運算元據。

EDM 與實體物件模型

我剛接觸 EDM 時恰好是與 EF Core 一起使用,就非常不理解這個現象:明明已經在 EF Core 中已經定義了模型,為啥還需要單獨設定一個 EDM?

其實,EDM 和實體框架(EF)Core 中的實體物件雖然都用於資料建模,但卻是不同的概念:在 EF Core 中,實體物件表示資料庫中的表或檢視,而 EDM 定義了 OData 服務中的資料結構,包括實體、屬性、導航屬性等。可以理解實體物件是為資料庫服務,而 EDM 是用於資料開放服務的

雖然 EDM 和 EF Core 中的實體物件都具有一些相似之處,例如它們都有屬性和關係,甚至你也可以直接返回實體模型用提供給 OData 讓其動態自動生成 EDM 模型(Non-ODM 模式),但是依然建議使用 EDM 模型,主要是有幾個方面:

  1. 實體框架中的實體類可能包含與 OData 服務定義不同的屬性。例如,實體框架中的實體類可能包含用於持久化和跟蹤狀態的屬性,而這些屬性可能並不需要在 OData 服務中公開。
  2. 實體框架中的實體類可能使用與 OData 服務定義不同的命名約定。例如,實體框架中的實體類可能使用 PascalCase(首字母大寫)命名約定,而在 OData 服務中的 EDM 可以使用 camelCase(首字母小寫)命名約定。
  3. 實體框架中的實體類可能包含與 OData 服務定義不同的資料結構。例如,聯合主鍵的實現用於 OData 查詢較為麻煩,可以設定 EDM 使得 OData 對外服務不使用聯合主鍵。

EDM 設定

在設定 OData 時,我們需要在程式碼中提供 EDM 物件。

.AddRouteComponents(AuthorizeHelper.PREFIX, EdmHelper.GetEdmModels());

GetEdmModels 函數返回一個 IEdmModel 物件。

      public static IEdmModel GetEdmModels()
        {
            ODataConventionModelBuilder builder = new ODataConventionModelBuilder();

            var device = builder.EntitySet<DeviceInfo>("DeviceInfos").EntityType.HasKey(p => p.DeviceId);
			device.Action("Upload");
            builder.EnableLowerCamelCase();
            return builder.GetEdmModel();
        }

以上程式碼中對 DeviceInfo 型別定義了一個實體物件,其具有 DeviceId 作為主鍵,擁有一個名為 Upload 的 Action。並且對所有的 EDM 物件啟用了 LowerCamelCase 支援。上面的感覺是挺簡單的是吧,注意我們使用到了 ODataConventionModelBuilder 物件,這個物件幫助我們自動實現了很多設定內容。如果我們使用其他的方式就不那麼簡單了。實際上設定 EDM 總共有三種方式。

我一般只使用 Convention 的設定方法,因此這裡參照官方網站的例子,詳情請見 Introduction to the model builders - OData | Microsoft Learn

Explicit

如果模型通過 new EdmModel() 構建,那麼構建的是無型別模型,相當於你不依賴現有的 CLR 型別憑空構建了一個模型。

public IEdmModel GetEdmModel()
{
    EdmModel model = new EdmModel();
    
	EdmEntityType customer = new EdmEntityType("WebApiDocNS", "Customer");
	customer.AddKeys(customer.AddStructuralProperty("CustomerId", EdmPrimitiveTypeKind.Int32));
	customer.AddStructuralProperty("Location", new EdmComplexTypeReference(address, isNullable: true));
	model.AddElement(customer);
	
	EdmEntityType order = new EdmEntityType("WebApiDocNS", "Order");
	order.AddKeys(order.AddStructuralProperty("OrderId", EdmPrimitiveTypeKind.Int32));
	order.AddStructuralProperty("Token", EdmPrimitiveTypeKind.Guid);
	model.AddElement(order);

    return model;
}

Non-convention

如果模型是依賴 new ODataModelBuilder() 構建,那麼構建模型時可以依據現有的 CLR 物件進行構建,不過依然需要設定每一個屬性、操作等。

public static IEdmModel GetEdmModel()
{
    var builder = new ODataModelBuilder();
    var customer = builder.EntityType<Customer>();
customer.HasKey(c => c.CustomerId);
customer.ComplexProperty(c => c.Location);
customer.HasMany(c => c.Orders);

	var order = builder.EntityType<Order>();
order.HasKey(o => o.OrderId);
order.Property(o => o.Token);
    return builder.GetEdmModel();
}

相當於前一種方法已經有了很大改進,我們可以依賴現有的結構,而不再需要手動去命名了。

Convention

更進一步,我們可以使用慣例 ( Convention )方式,模型依賴 new ODataConventionModelBuilder () 構建,這個是程式碼最少的,整個模型的設定按照 OData RESTful 慣例實現。

      public static IEdmModel GetEdmModels()
        {
            ODataConventionModelBuilder builder = new ODataConventionModelBuilder();

            var device = builder.EntitySet<DeviceInfo>("DeviceInfos").EntityType;
            return builder.GetEdmModel();
        }

Convention 涉及的內容很多,有機會以後會詳細解釋慣例生成 EDM 這種模式。

常用 EDM 設定

EDM 設定專案繁多,我們常用的有:

  • 設定實體(主鍵)
var device = builder.EntitySet<DeviceInfo>("DeviceInfos").EntityType;
  • 設定(集合) Action
//對於實體上的Action
device.Action("Upload");
//對於集合上的Action
device.Collection.Action("Upload");
  • 設定(集合) Function
//對於實體上的Function
device.Function("Data").Returns<string>();
//對於集合上的Function
device.Collection.Function("Data").Returns<string>();

對 Function 以及 Action 的詳細介紹,請期待後續文章。

存取 EDM 模型

OData 服務提供了 $metadata 終結點,可以從服務的根 URL 後新增 $metadata 來存取。例如以下是一個可以直接存取的 EDM 模型,以 XML 形式提供:

http://services.odata.org/TripPinRESTierService/$metadata

此外,也可以使用某些工具(如 Microsoft 的 OData Connected Service)來自動生成使用者端程式碼,並從服務中獲取後設資料。這些工具通常會從服務的根 URL 下載 $metadata 檔案,並將其解析為使用者端程式碼。