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

作者:Mike Wasson

下载已完成项目

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

本教程中使用的软件版本

在本教程中,我将演练如何创建调用 OData 服务的客户端应用程序。 OData 服务公开以下实体:

  • Product
  • Supplier
  • ProductRating

显示 O Data 服务实体及其属性列表的关系图,其中连接箭头显示每个实体如何相互关联或协同工作。

以下文章介绍如何在 Web API 中实现 OData 服务。 (你无需阅读它们就可以理解本教程。但是,)

生成服务代理

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

显示服务代理的 H T P 请求调用从应用程序、通过服务代理和 O 数据服务来回运行的示意图。

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

接下来,打开 Visual Studio 的另一个实例并创建控制台应用程序项目。 控制台应用程序将是 OData 客户端应用程序。 (还可以将项目添加到 service.)

注意

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

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

解决方案资源管理器窗口的屏幕截图,其中显示了用于添加新服务引用的“引用”下的菜单。

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

http://localhost:port/odata

其中 port 是端口号。

“添加服务引用”窗口的屏幕截图,其中显示了 U R L 地址字段中的端口号,以及用于添加名称空间的字段。

对于 “命名空间”,请键入“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 Data Services) 主题。

筛选 ($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

Client-Side分页 ($skip和$top)

对于大型实体集,客户端可能需要限制结果数。 例如,客户端可能一次显示 10 个条目。 这称为 客户端分页。 (还有 服务器端分页,其中服务器限制 results 的数量。) 若要执行客户端分页,请使用 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 方法。 例如,要包含 Supplier 每个 Product的 :

// 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 是实体集的名称。 例如, AddToProducts 向实体集添加新 ProductProducts 。 生成代理时,WCF Data Services会自动创建这些强类型 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 MERGE 请求。 PatchOnUpdate 选项指示 WCF 改为发送 HTTP PATCH。

注意

为什么使用 PATCH 与 MERGE? 原始 HTTP 1.1 规范 (RCF 2616) 未定义任何具有“部分更新”语义的 HTTP 方法。 为了支持部分更新,OData 规范定义了 MERGE 方法。 2010 年, RFC 5789 为部分更新定义了 PATCH 方法。 你可以在WCF Data Services博客上阅读此博客文章中的一些历史记录。 目前,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 元数据文档描述了这些操作,但代理类不会为其创建任何强类型方法。 你仍然可以使用泛型 Execute 方法调用 OData 操作。 但是,需要知道参数的数据类型和返回值。

例如,操作 RateProduct 采用类型 Int32 为“Rating”的参数,并返回 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();
        }
    }
}