使用 Web API 2 建立 OData v3 端點
演講者:Mike Wasson
開放式資料協定 (OData) 是一種網路資料存取協定。 OData 提供了一種統一的方式來建構資料、查詢資料以及透過 CRUD 操作 (建立、讀取、更新和刪除) 操作資料集。 OData 支援 AtomPub (XML) 和 JSON 格式。 OData 也定義了一種公開有關資料的中繼資料的方法。 用戶端可以使用中繼資料來發現資料集的類型資訊和關係。
ASP.NET Web API 可以輕鬆地為資料集建立 OData 端點。 您可以精確控制端點支援哪些 OData 操作。 您可以裝載多個 OData 端點以及非 OData 端點。 您可以完全控制資料模型、後端業務邏輯和資料層。
教學課程中使用的軟體版本
- Visual Studio 2013
- Web API 2
- OData 版本 3
- Entity Framework 6
- Fiddler Web Debugging Proxy (選用)
ASP.NET 和 Web Tools 2012.2 更新中新增了 Web API OData 支援。 但是,本教學課程使用 Visual Studio 2013 中新增的 Scaffolding。
在本教學課程中,您將建立一個用戶端可以查詢的簡單 OData 端點。 您還將為端點建立一個 C# 用戶端。 完成本教學課程後,下一組教學課程將展示如何新增更多功能,包括實體關係、動作和 $expand/$select。
建立 Visual Studio 專案
在本教學課程中,您將建立一個支援基本 CRUD 操作的 OData 端點。 端點將公開單一資源,即產品清單。 後續教學課程將增加更多功能。
啟動 Visual Studio,然後從「開始」頁面選擇「新專案」。 或者,從「檔案」功能表中選擇「新增」,然後選擇「專案」。
在「範本」窗格中,選擇「已安裝的範本」並展開「Visual C#」節點。 在「Visual C#」下,選擇「Web」。 選擇 ASP.NET Web 應用程式範本。
在「新 ASP.NET 專案」對話方塊中,選擇「空白」範本。 在「新增資料夾和核心參考...」下,選取 Web API。 按一下 [確定]。
新增實體模型
「模型」是代表應用程式中資料的物件。 對於本教學課程,我們需要一個代表產品的模型。 此模型對應於我們的 OData 實體類型。
在「方案總管」中,以滑鼠右鍵按一下「模型」資料夾。 從上下文功能表中,選擇「新增」,然後選擇「類別」。
在「新增」項目對話方塊中,將類別命名為「Product」。
注意
按照慣例,模型類別會放置在 Models 資料夾中。 您不必在自己的專案中遵循此慣例,但在本教學課程中我們會這麼做。
在 Product.cs 檔案中,新增以下類別定義:
public class Product
{
public int ID { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}
ID 屬性將作為實體索引鍵。 用戶端可以透過ID查詢產品。 此欄位也將是後端資料庫中的主鍵。
現在建置專案。 在下一步中,我們將使用一些 Visual Studio Scaffolding,透過反射來尋找產品類型。
新增 OData 控制器
控制器是處理 HTTP 請求的類別。 您可以為 OData 服務中的每個實體集定義一個單獨的控制器。 在本教學課程中,我們將建立一個控制器。
在方案總管中,以滑鼠右鍵按一下 Controllers 資料夾。 選擇「新增」,然後選擇「控制器」。
在「新增 Scaffold」對話方塊中,選擇「具有動作的 Web API 2 OData 控制器,使用 Entity Framework」。
在「新增控制器」對話方塊中,將控制器命名為「ProductsController」。 選取「使用非同步控制器動作」核取方塊。 在模型下拉式清單中,選擇「產品」類別。
按一下「新資料上下文...」按鈕。 保留資料上下文類型的預設名稱,然後按一下「新增」。
按一下「新增控制器」對話方塊中的「新增」以新增控制器。
請注意:如果收到錯誤訊息「取得類型時發生錯誤...」,請確保在新增「產品」類別後建置了 Visual Studio 專案。 Scaffolding 會使用反射來尋找類別。
Scaffolding 為專案新增兩個程式碼檔案:
- Products.cs 定義實作 OData 端點的 Web API 控制器。
- ProductServiceContext.cs 提供使用 Entity Framework 查詢底層資料庫的方法。
新增 EDM 和路由
在方案總管中,展開 App_Start 資料夾,並開啟名為 WebApiConfig.cs 的檔案。 此類別會保存 Web API 的設定碼。 以下列內容取代此程式碼:
using ProductService.Models;
using System.Web.Http;
using System.Web.Http.OData.Builder;
namespace ProductService
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");
config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());
}
}
}
這段程式碼會做兩件事:
- 為 OData 端點建立實體資料模型 (EDM)。
- 新增端點的路由。
EDM 是資料的抽像模型。 EDM 用於建立中繼資料文件並定義服務的 URI。 ODataConventionModelBuilder 會使用一組預設命名慣例 EDM 建立 EDM。 這種方法需要的程式碼最少。 如果您想對 EDM 擁有更多控制權,可以使用 ODataModelBuilder 類別,透過明確新增屬性、索引鍵和導覽屬性來建立 EDM。
EntitySet 方法會將實體集新增至 EDM:
modelBuilder.EntitySet<Product>("Products");
字串「Products」定義實體集的名稱。 控制器的名稱必須與實體集的名稱相符。 在本教學課程中,實體集名為「Products」,控制器名為 ProductsController
。 如果您將實體集命名為「ProductSet」,則控制器命名為 ProductSetController
。 請注意,一個端點可以有多個實體集。 為每個實體集呼叫 EntitySet<T>,然後定義對應的控制器。
MapODataRoute 方法為 OData 端點新增路由。
config.Routes.MapODataRoute("ODataRoute", "odata", model);
第一個參數是路由的易記名稱。 您的服務的用戶端看不到此名稱。 第二個參數是端點的 URI 首碼。 根據此程式碼,Products 實體集的 URI 為 http://hostname/odata/Products。 您的應用程式可以有多個 OData 端點。 對於每個端點,請呼叫 MapODataRoute,並提供唯一的路由名稱和唯一的 URI 首碼。
為資料庫新增種子 (選用)
在此步驟中,您將使用 Entity Framework 為資料庫新增一些測試資料。 此步驟是選擇性的,但這可以讓您立即測試 OData 端點。
從「工具」功能表中,選擇「NuGet 套件管理員」,然後選擇「套件管理員主控台」。 在「套件管理員主控台」視窗中,輸入以下命令:
Enable-Migrations
這將會新增一個名為 Migrations 的資料夾和一個名為 Configuration.cs 的程式碼檔案。
開啟此檔案並將以下程式碼新增至 Configuration.Seed
方法。
protected override void Seed(ProductService.Models.ProductServiceContext context)
{
// New code
context.Products.AddOrUpdate(new Product[] {
new Product() { ID = 1, Name = "Hat", Price = 15, Category = "Apparel" },
new Product() { ID = 2, Name = "Socks", Price = 5, Category = "Apparel" },
new Product() { ID = 3, Name = "Scarf", Price = 12, Category = "Apparel" },
new Product() { ID = 4, Name = "Yo-yo", Price = 4.95M, Category = "Toys" },
new Product() { ID = 5, Name = "Puzzle", Price = 8, Category = "Toys" },
});
}
在「套件管理員主控台」視窗中,輸入以下命令:
Add-Migration Initial
Update-Database
這些命令產生建立資料庫的程式碼,然後執行該程式碼。
探索 OData 端點
在本節中,我們將使用 Fiddler Web Debugging Proxy 將請求傳送到端點,並檢查回應訊息。 這將幫助您了解 OData 端點的功能。
在 Visual Studio 中,按 F5 開始偵錯。 預設情況下,Visual Studio 會將瀏覽器開啟到 http://localhost:*port*
,其中 port 是在專案設定中設定的連接埠號碼。
您可以在專案設定中變更連接埠號碼。 在「方案總管」中,以滑鼠右鍵按一下該專案並選擇「屬性」。 在屬性視窗中,選擇「Web」。 在專案 URL 下輸入連接埠號碼。
服務文件
服務文件包含 OData 端點的實體集清單。 若要取得服務文件,請向服務的根 URI 傳送 GET 請求。
使用 Fiddler,在 Composer 標籤中輸入以下 URI:http://localhost:port/odata/
,其中 port 是連接埠號碼。
按一下「執行」按鈕。 Fiddler 會向您的應用程式傳送 HTTP GET 請求。 您應該會在 Web 工作階段清單中看到回應。 如果一切正常,狀態代碼將為 200。
按兩下「Web 工作階段」清單中的回應可在「偵測器」標籤中查看回應訊息的詳細資訊。
原始 HTTP 回應訊息應類似於以下內容:
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/atomsvc+xml; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 17:51:01 GMT
Content-Length: 364
<?xml version="1.0" encoding="utf-8"?>
<service xml:base="http://localhost:60868/odata"
xmlns="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom">
<workspace>
<atom:title type="text">Default</atom:title>
<collection href="Products">
<atom:title type="text">Products</atom:title>
</collection>
</workspace>
</service></pre>
預設情況下,Web API 以 AtomPub 格式傳回服務文件。 若要請求 JSON,請將下列標頭新增至 HTTP 請求中:
Accept: application/json
現在 HTTP 回應包含 JSON 酬載:
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 22:59:28 GMT
Content-Length: 136
{
"odata.metadata":"http://localhost:60868/odata/$metadata","value":[
{
"name":"Products","url":"Products"
}
]
}
服務中繼資料文件
服務中繼資料文件使用稱為概念模式定義語言 (CSDL) 的 XML 語言描述服務的資料模型。 中繼資料文件會顯示服務中資料的結構,並且可用於產生用戶端程式碼。
若要取得中繼資料文件,請將 GET 請求傳送至 http://localhost:port/odata/$metadata
。 以下是本教學課程中顯示端點的中繼資料。
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/xml; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 23:05:52 GMT
Content-Length: 1086
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="1.0" xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx">
<edmx:DataServices m:DataServiceVersion="3.0" m:MaxDataServiceVersion="3.0"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
<Schema Namespace="ProductService.Models" xmlns="http://schemas.microsoft.com/ado/2009/11/edm">
<EntityType Name="Product">
<Key>
<PropertyRef Name="ID" />
</Key>
<Property Name="ID" Type="Edm.Int32" Nullable="false" />
<Property Name="Name" Type="Edm.String" />
<Property Name="Price" Type="Edm.Decimal" Nullable="false" />
<Property Name="Category" Type="Edm.String" />
</EntityType>
</Schema>
<Schema Namespace="Default" xmlns="http://schemas.microsoft.com/ado/2009/11/edm">
<EntityContainer Name="Container" m:IsDefaultEntityContainer="true">
<EntitySet Name="Products" EntityType="ProductService.Models.Product" />
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
實體集
若要取得 Products 實體集,請將 GET 請求傳送至 http://localhost:port/odata/Products
。
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 23:01:31 GMT
Content-Length: 459
{
"odata.metadata":"http://localhost:60868/odata/$metadata#Products","value":[
{
"ID":1,"Name":"Hat","Price":"15.00","Category":"Apparel"
},{
"ID":2,"Name":"Socks","Price":"5.00","Category":"Apparel"
},{
"ID":3,"Name":"Scarf","Price":"12.00","Category":"Apparel"
},{
"ID":4,"Name":"Yo-yo","Price":"4.95","Category":"Toys"
},{
"ID":5,"Name":"Puzzle","Price":"8.00","Category":"Toys"
}
]
}
Entity
若要取得單一產品,請向 http://localhost:port/odata/Products(1)
傳送 GET 請求,其中「1」是產品 ID。
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 23:04:29 GMT
Content-Length: 140
{
"odata.metadata":"http://localhost:60868/odata/$metadata#Products/@Element","ID":1,
"Name":"Hat","Price":"15.00","Category":"Apparel"
}
OData 序列化格式
OData 支援多種序列化格式:
- Atom Pub (XML)
- JSON「精簡」(在 OData v3 中引入)
- JSON「詳細」(OData v2)
預設情況下,Web API 使用 AtomPubJSON「精簡」格式。
若要取得 AtomPub 格式,請將 Accept 標頭設定為「application/atom+xml」。 以下是回應本文範例:
<?xml version="1.0" encoding="utf-8"?>
<entry xml:base="http://localhost:60868/odata" xmlns="http://www.w3.org/2005/Atom" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns:georss="http://www.georss.org/georss" xmlns:gml="http://www.opengis.net/gml">
<id>http://localhost:60868/odata/Products(1)</id>
<category term="ProductService.Models.Product" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
<link rel="edit" href="http://localhost:60868/odata/Products(1)" />
<link rel="self" href="http://localhost:60868/odata/Products(1)" />
<title />
<updated>2013-09-23T23:42:11Z</updated>
<author>
<name />
</author>
<content type="application/xml">
<m:properties>
<d:ID m:type="Edm.Int32">1</d:ID>
<d:Name>Hat</d:Name>
<d:Price m:type="Edm.Decimal">15.00</d:Price>
<d:Category>Apparel</d:Category>
</m:properties>
</content>
</entry>
您可以看到 Atom 格式有個明顯的缺點:它比 JSON 詳細得多。 但是,如果您有一個理解 AtomPub 的用戶端,則該用戶端可能更偏好使用該格式而不是 JSON。
以下是同一實體的 JSON 精簡版:
{
"odata.metadata":"http://localhost:60868/odata/$metadata#Products/@Element",
"ID":1,
"Name":"Hat",
"Price":"15.00",
"Category":"Apparel"
}
JSON 精簡格式是在 OData 協定版本 3 中引入的。 為了向後相容,用戶端可以請求較舊的「詳細」JSON 格式。 若要請求詳細 JSON,請將 Accept 標頭設為 application/json;odata=verbose
。 這是詳細版本:
{
"d":{
"__metadata":{
"id":"http://localhost:18285/odata/Products(1)",
"uri":"http://localhost:18285/odata/Products(1)",
"type":"ProductService.Models.Product"
},"ID":1,"Name":"Hat","Price":"15.00","Category":"Apparel"
}
}
這種格式在回應本文中會傳遞更多中繼資料,這可能會在整個工作階段中增加相當大的額外負荷。 此外,它還透過將物件包裝在名為「d」的屬性中來新增間接層級。