从 .NET 客户端调用 OData 服务 (C#)

作者: Mike Wasson

下载完成的项目

本教程演示如何从C#客户端应用程序调用 OData 服务。

本教程中使用的软件版本

在本教程中,我将逐步介绍如何创建一个调用 OData 服务的客户端应用程序。 OData 服务公开以下实体:

  • Product
  • Supplier
  • ProductRating

以下文章介绍如何在 Web API 中实现 OData 服务。 (但是,您不需要阅读它们来了解本教程。)

生成服务代理

第一步是生成服务代理。 服务代理是一个 .NET 类,用于定义用于访问 OData 服务的方法。 代理会将方法调用转换为 HTTP 请求。

首先,在 Visual Studio 中打开 "OData 服务" 项目。 在 IIS Express 中按 CTRL + F5 以本地方式运行该服务。 记下本地地址,包括 Visual Studio 分配的端口号。 创建代理时,将需要此地址。

接下来,打开 Visual Studio 的另一个实例,并创建一个控制台应用程序项目。 控制台应用程序将是我们的 OData 客户端应用程序。 (您也可以将该项目添加到与该服务相同的解决方案中。)

Note

其余步骤引用控制台项目。

在解决方案资源管理器中,右键单击 "引用",然后选择 "添加服务引用"。

在 "添加服务引用" 对话框中,键入 OData 服务的地址:

http://localhost:port/odata

其中port是端口号。

对于 "命名空间",键入 "ProductService"。 此选项定义代理类的命名空间。

单击 “转到” 。 Visual Studio 将读取 OData 元数据文档,以发现服务中的实体。

单击 "确定" 将代理类添加到项目。

创建服务代理类的实例

Main 方法中,创建代理类的新实例,如下所示:

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。 不需要更新代理。

下面的代码将添加一个事件处理程序,用于将请求 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 集合时,代理类会自动执行此类。

运行应用程序时,输出应如下所示:

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 定义可用于对数据进行筛选、排序、分页等的查询选项。 在服务代理中,可以通过使用各种 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'

请注意,代理将 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 SkipTake方法。 下面的示例跳过前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 方法。 例如,若要包括每个 ProductSupplier

// 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;在这种情况下,代理将自动包括扩展。 下面的示例获取了每个产品的名称和供应商。

// 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是实体集的名称。 例如,AddToProductsProducts 实体集添加新 Product。 生成代理时,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);
    }
}

若要在两个实体之间添加链接,请使用AddLinkSetLink方法。 以下代码将添加新的供应商和新产品,然后在它们之间创建链接。

// 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 合并请求。 PatchOnUpdate选项告知 WCF 改为发送 HTTP 修补程序。

Note

为什么要修补与合并? 原始 HTTP 1.1 规范(RCF 2616)未定义具有 "部分更新" 语义的任何 HTTP 方法。 为了支持部分更新,OData 规范定义了 MERGE 方法。 在2010中, RFC 5789为部分更新定义了修补程序方法。 你可以在 WCF 数据服务博客上阅读此博客文章中的部分历史记录。 目前,修补程序优先于 MERGE。 Web API 基架创建的 OData 控制器支持这两种方法。

如果要替换整个实体(PUT 语义),请指定带 savechangesoptions.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 操作使用 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();

有关详细信息,请参阅调用服务操作和操作

一种选择是扩展容器类,以提供调用该操作的强类型方法:

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();
        }
    }
}