演講者:Mike Wasson
本教學課程示範如何從 C# 用戶端應用程式呼叫 OData 服務。
教學課程中使用的軟體版本
- Visual Studio 2013 (與 Visual Studio 2012 搭配使用)
- WCF Data Services 用戶端程式庫
- Web API 2。 (範例 OData 服務是使用 Web API 2 建置的,但用戶端應用程式不相依於 Web API。)
在本教學課程中,我將逐步建立一個呼叫 OData 服務的用戶端應用程式。 OData 服務公開以下實體:
ProductSupplierProductRating

以下文章介紹如何在 Web API 中實作 OData 服務。 (但是,您不需要閱讀它們來理解本教學課程。)
產生服務 Proxy
第一步是產生服務 Proxy。 服務 Proxy 程式是一個 .NET 類別,它定義了存取 OData 服務的方法。 Proxy 將方法呼叫轉換為 HTTP 請求。

首先在 Visual Studio 中開啟 OData 服務專案。 按 CTRL+F5 在 IIS Express 中本機執行該服務。 記下本機位址,包括 Visual Studio 指派的連接埠號碼。 建立 Proxy 時您將需要此位址。
接下來,打開 Visual Studio 的另一個執行個體並建立一個主控台應用程式專案。 主控台應用程式將是我們的 OData 用戶端應用程式。 (您也可以將專案新增至與服務相同的解決方案。)
注意
其餘步驟參考主控台專案。
在「方案總管」中,以滑鼠右鍵按一下「參考」並選擇「新增服務參考」。

在「新增服務參考」對話方塊中,鍵入 OData 服務的位址:
http://localhost:port/odata
其中 port 是連接埠號碼。
在命名空間,輸入「ProductService」。 此選項定義 Proxy 類別的命名空間。
按一下 [ Go]。 Visual Studio 讀取 OData 中繼資料文件以發現服務中的實體。
按一下「確定」將 Proxy 類別新增至您的專案。

建立服務 Proxy 類別的執行個體
在您的 Main 方法中,建立 Proxy 類別的新執行個體,如下所示:
using System;
using System.Data.Services.Client;
using System.Linq;
namespace Client
{
class Program
{
static void Main(string[] args)
{
Uri uri = new Uri("http://localhost:1234/odata/");
var container = new ProductService.Container(uri);
// ...
}
}
}
再次,使用執行服務的實際連接埠號碼。 當您部署服務時,您將使用即時服務的 URI。 您不需要更新 Proxy 程式。
以下程式碼新增事件處理程序,用於將請求 URI 列印到主控台視窗。 此步驟不是必需的,但查看每個查詢的 URI 很有趣。
container.SendingRequest2 += (s, e) =>
{
Console.WriteLine("{0} {1}", e.RequestMessage.Method, e.RequestMessage.Url);
};
查詢服務
以下程式碼從 OData 服務取得產品清單。
class Program
{
static void DisplayProduct(ProductService.Product product)
{
Console.WriteLine("{0} {1} {2}", product.Name, product.Price, product.Category);
}
// Get an entire entity set.
static void ListAllProducts(ProductService.Container container)
{
foreach (var p in container.Products)
{
DisplayProduct(p);
}
}
static void Main(string[] args)
{
Uri uri = new Uri("http://localhost:18285/odata/");
var container = new ProductService.Container(uri);
container.SendingRequest2 += (s, e) =>
{
Console.WriteLine("{0} {1}", e.RequestMessage.Method, e.RequestMessage.Url);
};
// Get the list of products
ListAllProducts(container);
}
}
請注意,您不需要編寫任何程式碼來傳送 HTTP 請求或解析回應。 當您在 foreach 迴圈中列舉 Container.Products 集合時,Proxy 類別會自動執行此操作。
執行該應用程式時,輸出應如下所示:
GET http://localhost:60868/odata/Products
Hat 15.00 Apparel
Scarf 12.00 Apparel
Socks 5.00 Apparel
Yo-yo 4.95 Toys
Puzzle 8.00 Toys
若要透過 ID 取得實體,請使用 where 子句。
// Get a single entity.
static void ListProductById(ProductService.Container container, int id)
{
var product = container.Products.Where(p => p.ID == id).SingleOrDefault();
if (product != null)
{
DisplayProduct(product);
}
}
對於本主題的其餘部分,我不會顯示整個 Main 函式,僅顯示呼叫該服務所需的程式碼。
應用程式查詢選項
OData 定義可用於篩選、排序、分頁資料等的查詢選項。 在服務 Proxy 中,您可以透過使用各種 LINQ 運算式來套用這些選項。
在本節中,我將展示一個簡短的範例。 有關更多詳細資訊,請參閱 MSDN 上的主題「LINQ 注意事項 (WCF 資料服務)」。
篩選 ($filter)
若要篩選,請使用 where 子句。 以下範例按產品類別進行篩選。
// Use the $filter option.
static void ListProductsInCategory(ProductService.Container container, string category)
{
var products =
from p in container.Products
where p.Category == category
select p;
foreach (var p in products)
{
DisplayProduct(p);
}
}
此程式碼對應以下 OData 查詢。
GET http://localhost/odata/Products()?$filter=Category eq 'apparel'
請注意,Proxy 會將 where 子句轉換為 OData $filter 運算式。
排序 ($orderby)
若要排序,請使用 orderby 子句。 以下範例按價格從最高到最低排序。
// Use the $orderby option
static void ListProductsSorted(ProductService.Container container)
{
// Sort by price, highest to lowest.
var products =
from p in container.Products
orderby p.Price descending
select p;
foreach (var p in products)
{
DisplayProduct(p);
}
}
這是對應的 OData 請求。
GET http://localhost/odata/Products()?$orderby=Price desc
用戶端分頁 ($skip 和 $top)
對於大型實體集,用戶端可能希望限制結果的數量。 例如,用戶端可能一次顯示 10 個項目。 這稱為用戶端分頁。 (還有伺服器端分頁,伺服器限制結果的數量。) 要執行用戶端分頁,請使用 LINQ Skip 和 Take 方法。 以下範例跳過前 40 個結果並取得接下來的 10 個結果。
// Use $skip and $top options.
static void ListProductsPaged(ProductService.Container container)
{
var products =
(from p in container.Products
orderby p.Price descending
select p).Skip(40).Take(10);
foreach (var p in products)
{
DisplayProduct(p);
}
}
這是對應的 OData 請求:
GET http://localhost/odata/Products()?$orderby=Price desc&$skip=40&$top=10
選擇 ($select) 和展開 ($expand)
若要包含相關實體,請使用 DataServiceQuery<t>.Expand 方法。 例如,為每個 Product 加入 Supplier:
// Use the $expand option.
static void ListProductsAndSupplier(ProductService.Container container)
{
var products = container.Products.Expand(p => p.Supplier);
foreach (var p in products)
{
Console.WriteLine("{0}\t{1}\t{2}", p.Name, p.Price, p.Supplier.Name);
}
}
這是對應的 OData 請求:
GET http://localhost/odata/Products()?$expand=Supplier
若要變更回應的形狀,請使用 LINQ select 子句。 以下範例僅取得每個產品的名稱,沒有其他屬性。
// Use the $select option.
static void ListProductNames(ProductService.Container container)
{
var products = from p in container.Products select new { Name = p.Name };
foreach (var p in products)
{
Console.WriteLine(p.Name);
}
}
這是對應的 OData 請求:
GET http://localhost/odata/Products()?$select=Name
select 子句可以包含相關實體。 在這種情況下,不要呼叫 Expand;在此案例中,Proxy 會自動加入擴充功能。 以下範例取得每個產品的名稱和供應商。
// Use $expand and $select options
static void ListProductNameSupplier(ProductService.Container container)
{
var products =
from p in container.Products
select new
{
Name = p.Name,
Supplier = p.Supplier.Name
};
foreach (var p in products)
{
Console.WriteLine("{0}\t{1}", p.Name, p.Supplier);
}
}
這是對應的 OData 請求。 請注意,它包含 $expand 選項。
GET http://localhost/odata/Products()?$expand=Supplier&$select=Name,Supplier/Name
有關 $select 和 $expand 的更多資訊,請參閱「在 Web API 2 中使用 $select、$expand 和 $value」。
新增實體
若要將新實體新增至實體集,請呼叫 AddToEntitySet,其中 EntitySet 是實體集的名稱。 例如,AddToProducts 將新的 Product 新增至 Products 實體集。 產生 Proxy 時,WCF 資料服務會自動建立這些強型別 AddTo 方法。
// Add an entity.
static void AddProduct(ProductService.Container container, ProductService.Product product)
{
container.AddToProducts(product);
var serviceResponse = container.SaveChanges();
foreach (var operationResponse in serviceResponse)
{
Console.WriteLine(operationResponse.StatusCode);
}
}
若要在兩個實體之間新增連結,請使用 AddLink 和 SetLink 方法。 以下程式碼會新增供應商和新產品,然後在它們之間建立連結。
// Add entities with links.
static void AddProductWithSupplier(ProductService.Container container,
ProductService.Product product, ProductService.Supplier supplier)
{
container.AddToSuppliers(supplier);
container.AddToProducts(product);
container.AddLink(supplier, "Products", product);
container.SetLink(product, "Supplier", supplier);
var serviceResponse = container.SaveChanges();
foreach (var operationResponse in serviceResponse)
{
Console.WriteLine(operationResponse.StatusCode);
}
}
當導覽屬性是集合時,請使用 AddLink。 在此範例中,我們將產品新增至供應商的 Products 集合。
當導覽屬性是單一實體時,請使用 SetLink。 在此範例中,我們要設定產品的 Supplier 屬性。
更新/修補檔
若要更新實體,請呼叫 UpdateObject 方法。
static void UpdatePrice(ProductService.Container container, int id, decimal price)
{
var product = container.Products.Where(p => p.ID == id).SingleOrDefault();
if (product != null)
{
product.Price = price;
container.UpdateObject(product);
container.SaveChanges(SaveChangesOptions.PatchOnUpdate);
}
}
當您呼叫 SaveChanges 時會執行更新。 預設情況下,WCF 會傳送 HTTP MERGE 請求。 PatchOnUpdate 選項告訴 WCF 改為傳送 HTTP PATCH。
注意
為什麼選擇 PATCH 與 MERGE? 最初的 HTTP 1.1 規格 (RCF 2616) 並沒有定義任何具有「部分更新」語意的 HTTP 方法。 為了支援部分更新,OData 規範定義了 MERGE 方法。 2010 年,RFC 5789 定義了部分更新的 PATCH 方法。 您可以在 WCF 資料服務部落格上的這篇文章中閱讀一些歷史記錄。 如今,PATCH 比 MERGE 更受青睞。 Web API 腳手架建立的 OData 控制器支援這兩種方法。
如果要取代整個實體 (PUT 語意),請指定 ReplaceOnUpdate 選項。 這會導致 WCF 傳送 HTTP PUT 請求。
container.SaveChanges(SaveChangesOptions.ReplaceOnUpdate);
刪除實體
若要刪除實體,請呼叫 DeleteObject。
static void DeleteProduct(ProductService.Container container, int id)
{
var product = container.Products.Where(p => p.ID == id).SingleOrDefault();
if (product != null)
{
container.DeleteObject(product);
container.SaveChanges();
}
}
叫用 OData 動作
在 OData 中,動作是新增伺服器端行為的一種方法,這些行為不容易定義為實體上的 CRUD 操作。
儘管 OData 中繼資料文件描述了動作,但 Proxy 類別不會為它們建立任何強類型方法。 您仍然可以使用通用 Execute 方法呼叫 OData 動作。 但是,您需要知道參數的資料類型和傳回值。
例如,RateProduct 動作採用名為「Rating」的參數,類型為 Int32,並傳回 double。 以下程式碼顯示如何叫用此動作。
int rating = 2;
Uri actionUri = new Uri(uri, "Products(5)/RateProduct");
var averageRating = container.Execute<double>(
actionUri, "POST", true, new BodyOperationParameter("Rating", rating)).First();
有關更多資訊,請參閱「呼叫服務操作和動作」。
一種選擇是擴展 Container 類別以提供叫用該動作的強類型方法:
namespace ProductServiceClient.ProductService
{
public partial class Container
{
public double RateProduct(int productID, int rating)
{
Uri actionUri = new Uri(this.BaseUri,
String.Format("Products({0})/RateProduct", productID)
);
return this.Execute<double>(actionUri,
"POST", true, new BodyOperationParameter("Rating", rating)).First();
}
}
}

