從 .NET 用戶端呼叫 OData 服務 (C#)
演講者:Mike Wasson
本教學課程示範如何從 C# 用戶端應用程式呼叫 OData 服務。
教學課程中使用的軟體版本
- Visual Studio 2013 (與 Visual Studio 2012 搭配使用)
- WCF Data Services 用戶端程式庫
- Web API 2。 (範例 OData 服務是使用 Web API 2 建置的,但用戶端應用程式不相依於 Web API。)
在本教學課程中,我將逐步建立一個呼叫 OData 服務的用戶端應用程式。 OData 服務公開以下實體:
Product
Supplier
ProductRating
以下文章介紹如何在 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();
}
}
}