65.9K
CodeProject 正在变化。 阅读更多。
Home

OData 服务

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (93投票s)

2012年6月1日

CPOL

29分钟阅读

viewsIcon

416010

downloadIcon

7317

如何使用 WCF Data Services 创建 OData 服务并使用它们。

目录

  1. 引言
  2. OData 协议概述
    1. 格式化输出
    2. 读取数据
    3. 字段投影
    4. 展开
    5. 分页
    6. 过滤
    7. 结果排序
    8. 更新数据
  3. 使用 WCF Data Service 创建 OData 服务
    1. 数据提供程序实现
    2. WCF 服务自定义
    3. 拦截器
    4. 服务操作
  4. 使用 OData 服务
    1. LINQPad
    2. C# 代码示例
    3. JavaScript 代码示例
    4. 其他工具/库
  5. 结论
  6. 历史

引言

标准 Web 服务使您能够创建一些返回或更新数据的函数或过程。基本思想是,您允许使用者通过 Web 使用 HTTP 协议访问这些函数。这是面向服务架构 (SOA) 的基础。然而,面向服务架构存在一些缺点。您永远无法预测使用者需要什么样的查询/服务,因此您总是需要为现有服务添加新服务、添加参数或返回值,仅仅因为某些使用者需要一些特定的东西。

另一种方法是所谓的面向资源架构 (ROA),您在此方法中公开资源并允许用户针对这些资源运行各种临时查询。这类似于数据库系统中的方法,其中您拥有代表数据的表以及创建各种 SQL 查询的能力。唯一的区别是,在 ROA 中,您通过 URL 创建查询。

OData 是一种协议,它规定了公开数据的 Web 服务的特性。在本文中,我将向您展示 OData 协议是什么,以及如何使用 WCF Data Service 技术实现符合该协议的 Web 服务。在接下来的部分中,我将解释以下内容:

  1. OData 协议是什么以及如何使用 URL 查询数据
  2. 如何使用 WCF Data Services 创建自己的 OData 服务
  3. 如何使用各种工具和代码示例调用 OData 服务

OData 协议概述

如上所述,OData 服务是公开某些资源的 Web 服务。您可以通过 URL 访问这些资源。OData 协议规定了如何通过 HTTP 查询访问数据。基本思想是,您可以打开一个带有参数的 URL,该 URL 将针对资源执行查询。

目前,有很多公共 OData 服务供您使用。您可以在 OData 网站 上找到完整列表 [^] - 其中一些是:

因此,我将使用将在真实公共 OData 服务上执行的实际查询来描述 OData 协议。在第一个示例中,我将向您展示如何查找 OData Web 服务公开的所有资源。如果您打开某个 OData 的基 URL,例如 http://services.odata.org/OData/OData.svc/,您将看到它公开的资源列表。

在这里,您可以看到服务公开的三个资源集合。要访问这些资源,您需要在 URL 中添加资源的名称。

这些查询将以 Xml-Atom 格式返回资源的完整列表。下面是针对 Products 查询返回的响应示例。

您可以加载和解析此 XML,也可以使用一些读取它的 RSS feed 工具。

格式化数据

除了默认的 Xml-Atom 格式外,您还可以使用其他格式 - 目前支持 JSON 格式。如果您将参数 $format=json 添加到 URL 中,您可以以 JSON 格式返回数据,如下面的示例所示:

在撰写本文时,上述 URL 返回的是所谓的“Verbose JSON”,其中大量元数据信息包含在响应中。有一项倡议是将格式更改为“Light JSON”,其中返回的结果将仅包含最少的元数据并减小响应大小。这仍处于实验阶段,但您可以通过以下 URL 了解其外观:

将来,当前的“Verbose JSON”格式仍将可用,但需要使用 $format=verbosejson 显式请求,如下面的示例所示:

但是,请注意这仍处于实验阶段,尚未正式发布。一旦该实验版本变为官方版本(或被放弃),这些实验 URL 将不再可用。

使用这些 URL 查询,您可以获取完整的数据列表;然而,OData 使您能够切片、筛选和获取数据子集。在接下来的部分中,您可以找到更多关于 OData 服务用法示例。

读取数据

除了获取所有信息外,您还可以创建 URL 查询来查找有关某些特定资源的信息。例如,您可以通过在方括号中添加 id 值来按 id 查找资源。以下示例显示了如何仅返回 id 为 1 的类别:

此外,您可以通过添加 */Resource 后缀来获取某个对象的关联信息。一些示例是:

使用这些查询,您可以找到与某些实体相关的任何信息。

字段投影

默认情况下,将返回资源中的所有字段;然而,您可以指定要在查询中返回的字段子集。例如,如果您只想获取产品的 ID 和名称,您将使用以下查询:

如果您想返回资源的复杂子类型,这也适用。例如,如果供应商有包含 Address、Town、State 和 Postcode 子字段的 Address 属性,您可以使用以下查询指定 Address 对象将被返回:

选择子类型时,您将获得地址对象的所有字段。不幸的是,没有办法只选择子对象的单个属性,因此返回仅 State 字段的 Address 资源的查询将会失败。

展开

当您查询资源时,只会返回直接字段,而不会包含关联实体。例如,如果您按 id 获取产品,则不会包含有关相关供应商和类别的信息。下面是扩展 JSON 格式响应的图示:

正如您所见,它会告诉您 Categorysuppliers 对象被延迟加载,并且您有一个可以用于获取这些资源的链接。如果您希望这些信息与原始数据一起返回,您将需要使用 $expand 系统参数。展开与投影相反。使用 $expand 时,您不是限制字段列表,而是包含其他关联实体。返回 Product 和关联的 SupplierCategory 的查询示例是:

对于第二个 URL,如果您使用扩展 JSON 格式作为输出,您将能够看到供应商信息已被包含。

正如您所见,supplier 对象已被包含,但 supplier 产品未被包含。要包含它们,您需要更改 $expand 的值 - /Products(1)?$format=json&$expand=Supplier/Products。请注意,展开级别的深度可能受到服务器端限制。

分页

除了返回整个结果集之外,您还可以将其分成块并实现某种分页。有以下参数可用于实现分页:

  • $top – 指定当前响应中应返回多少结果。
  • $skip – 指定应跳过多少满足当前条件的记录。
  • $skiptoken – 指定从开始到某个 id 的所有结果都将被跳过。
  • $inlinecount – 如果设置为 allpages,则要求返回记录的总数。

以下示例显示了一个返回第 20 到第 30 条记录并返回总数的 URL 查询示例:

如果结果作为 Atom-xml feed 返回,您将在 <m:count> 标签中获得额外信息,如下面的示例所示。

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<feed>
    <title type="text">Products</title>
    <id>http://services.odata.org/Northwind/Northwind.svc/Products</id>
    <updated>2012-05-23T15:59:18Z</updated>
    <link rel="self" title="Products" href="Products" />
    <m:count>77</m:count>
    ...
</feed> 

在 JSON(Verbose)格式中,您将获得一个额外的 __count 属性,其中包含实体总数。

{
"d" : {
        "results": [

            <<results>>

        ],
        "__count": "77"
     }
}  

系统参数 $skiptoken 是另一种分割结果的方法。如果您在 $skiptoken=<<ID>> 部分指定实体 ID,那么从开始到具有键 <<ID>> 的实体之间的所有内容都将被跳过。

请注意,一些 OData Web 服务默认最多返回 (N) 个结果,并提供指向下一页的链接。例如,如果您想以 json 格式获取 Northwind OData 服务中的所有客户,例如 /Customers?$format=json,您将在一个响应中获得有限数量的结果,并在 __next 属性中获得指向下一组资源的链接,如下面的示例所示。

{
"d" : {
        "results": [

            <<results>>

        ],
        "__next": 
        "http://services.odata.org/Northwind/Northwind.svc/Customers?$skiptoken='ERNSH'"
}
}  

如果您遵循该链接,您将获得下一组 (N) 个结果。请注意,这是服务器端限制,不取决于您在 $top 参数中的值。如果您尝试在 Northwind OData 服务上运行以下查询 /Products?$top=100&$inlinecount=allpages&$format=json,其中您要求返回 100 个结果,您将获得 20 个条目,格式如下:

{
"d" : {
        "results": [
 
               <<20 results>>

        ],
        "__count": "77",
        "__next": "http://services.odata.org/Northwind/Northwind.svc/
                   Products?$inlinecount=allpages&$top=80&$skiptoken=20"
     }
}

正如您所见,尽管所有资源的总数(77)少于要求的 $top 值(100),Northwind 服务只会返回 20 个条目,并提供指向下一分区的链接。这是服务器端设置,您无法通过 URL 查询进行控制,除非您创建自己的 OData Web 服务。

筛选

在前面的示例中,您已经看到了如何按 ID 筛选资源,例如,/Products(1)。然而,相同的查询也可以使用 $filter 参数创建,您可以在其中明确指定按 id 筛选产品,例如,/Products?$filter=ID eq 1。系统参数 $filter 使您能够按其他属性指定任何条件。例如,您可以按名称筛选产品,如下面的示例所示:/Products?$filter=Name eq 'Milk'。在本节中,您可以找到 $filter 系统选项的各种用法。

关系运算符

在前面的示例中,您已经看到了如何使用 eq 运算符进行筛选。OData 协议允许您使用其他关系运算符,如不等于 (ne)、小于 (lt)、小于或等于 (le)、大于 (gt)、大于或等于 (ge)。一些示例是:

逻辑运算符

您可以使用逻辑运算符 andornot 和括号创建复杂查询。下面的示例显示了一个复杂查询的示例:

算术运算

您可以应用标准运算符来执行加法 (add)、减法 (sub)、乘法 (mul)、除法 (div) 或求余数 (mod)。返回所有总库存价值(单价 * 库存单位)小于 45(并且有库存)的所有产品的查询示例是:

请注意,目前,您只能在 $filter 条件中使用算术函数,而在 $select 中不能使用。

数值函数

如果您的属性是数字,您可以应用 floorceilinground 函数。使用这些函数的查询示例是:/Products?$filter=floor(Price) eq 3 or ceiling(Price) eq 3

字符串函数

您可以在筛选表达式中使用许多 string 函数 - 其中一些(附带示例)是:

日期函数

如果您的资源中有 datetime 属性,您可以使用几个日期部分函数,如 year()month()day()hour()minute()second()。例如,返回 OrderDate1996 年的所有订单的 URL 查询是:

结果排序

另一个有用的功能是排序。您可以使用 $orderby 查询选项按某个属性或函数对结果进行排序。默认顺序是升序,但您可以更改它。一些示例是:

更新数据

OData 服务是 REST 服务,因此也允许数据修改操作。在 REST 服务中,通过发送的 HTTP 请求类型使用以下规则定义数据访问操作的类型:

  • HTTP GET 请求用于读取数据。
  • HTTP POST 请求用于更新现有实体。
  • HTTP PUT 请求用于创建新实体。
  • HTTP DELETE 请求用于删除实体。

HTTP GET 协议是您通过浏览器打开 URL 时的默认协议,因此我用它来展示各种读取数据的示例。然而,其他请求我们无法通过浏览器(没有某种形式)运行,因此通常通过一些客户端库执行,该库具有允许您更新数据的 API。由于您永远不会直接使用 HTTP 协议,并且每个数据修改请求都取决于您使用的库,因此我在这里不展示示例。但是,您应该知道这是可能的,并且您可以使用任何客户端库轻松执行它。

请注意,在关于使用 C# 代码与 OData 服务交互的部分中,您可以找到如何使用 C# 代码更新 OData 资源的示例。该代码直接转换为更新服务中数据的 HTTP 查询。

使用 WCF Data Services 创建 OData Web 服务

在上一节中,您看到了如何使用一些符合 OData 协议的公共 Web 服务。然而,如果您了解 OData 规范,您就可以创建自己的服务 - 它只需要提供 OData 规范要求的一切。在本节中,我将展示如何使用 WCF Data Services 创建自己的符合 OData 的 Web 服务。

WCF Data Services(以前称为 ADO.NET Services)使您能够创建面向资源的 Web 服务。但是,您必须理解 WCF Data Services 不是 OData 服务 - 它们非常接近 OData 规范,但它们并非 100% 兼容。WCF Data Service 实现提供的大部分功能都符合 OData 规范,但有一些调整可能需要手动进行,以便使 WCF 服务与 OData 规范保持一致。

创建 WCF 服务很简单 - 只需转到 **添加新项** > **Web** > **WCF Data Service**,输入名称(例如,MyWcfDataService),Visual Studio 将生成以下源代码。

public class MyWcfDataService : DataService< /* TODO: put your data source class name here */ >
{
	// This method is called only once to initialize service-wide policies.
	public static void InitializeService(DataServiceConfiguration config)
	{
		// TODO: set rules to indicate which entity sets and service operations are visible, 
        // updatable, etc.
		// Examples:
		// config.SetEntitySetAccessRule("MyEntityset", EntitySetRights.AllRead);
		// config.SetServiceOperationAccessRule
        // ("MyServiceOperation", ServiceOperationRights.All);

		config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
	}
} 

这是一个公开数据源提供程序类中资源的模板。现在您需要在 `< >` 部分设置数据源提供程序。数据源提供程序是一个类,它包含代表应由服务公开的资源属性。这些属性的类型要么实现 IQueryable(对于只读实体),要么实现 IUpdateable(对于可写实体),或者同时实现这两个接口。数据源提供程序类的示例显示在以下列表中:

public class MyDataSourceProvider{

	public IQueriable<Product> Products { get; }

	public IQueriable<Supplier> Suppliers { get; }

	public IUpdateable<Category> Categories { get; }
}

此数据源提供程序使您能够将 ProductsCategories 作为只读属性公开,并将 Categories 作为可写资源公开。如果您在服务定义中放置 MyDataSourceProvider 类,您将能够公开提供程序中的资源。

public class MyWcfDataService : DataService< MyDataSourceProvider >
{

}

将数据源提供程序与 WCF 服务关联后,您需要在服务初始化方法中定义访问权限。以下代码允许完全访问供应商,读取访问产品,以及写入访问类别。

public static void InitializeService(DataServiceConfiguration config)
{
	config.SetEntitySetAccessRule("Suppliers", EntitySetRights.All)
	config.SetEntitySetAccessRule("Products", EntitySetRights.AllRead);
	config.SetEntitySetAccessRule("Categories", EntitySetRights.AllWrite);

	config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
} 

在接下来的部分中,我们将看到如何自定义此 WCF Data Service。

数据源提供程序实现

数据源提供程序是服务最重要的部分,因为它将充当查询和更新数据的代理。在本节中,您将看到如何为纯内存对象列表和数据库实现数据源提供程序。

反射数据源提供程序

我们可以为任何数据源创建提供程序 - 即使是内存对象的 static 列表。该类在以下示例中所示:

public class DataRepository
{
    public static List<Product> Products = new List<Product>();

    public static List<Supplier> Suppliers = new List<Supplier>();
} 

在这里,我们可以看到一个包含产品和供应商列表的 static 类。我不关心这些列表是如何填充的。为了使用 WCF 数据服务公开这些对象,我们需要创建一个具有 IQueryable 属性的提供程序 - 示例显示在以下列表中:

public class MyWCFDataSource
{
    public IQueryable<Product> MyProducts
    {
        get
        {
            return DataRepository.Products.AsQueryable();
        }
    }

    public IQueryable<Supplier> MySuppliers
    {
        get
        {
            return DataRepository.Suppliers.Where(s => !s.IsArchived).AsQueryable();
        }
    }

    public MyWCFDataSource()
    {
        this.MyLondonSuppliers = DataRepository.Suppliers
                                    .Where(s=>s.County == "London")
                                    .Select(supplier => new LondonSupplier()
                                    {
                                        ID = supplier.SupplierID,
                                        Name = supplier.Name,
                                        Address = supplier.Address
                                    })
                                    .AsQueryable();
    }

    public IQueryable<LondonSupplier> MyLondonSuppliers
    {
        get;
        private set;
    }
} 

在这里,我放置了一个标准的 IQueryable 属性,该属性在将 Products 转换为 Queryable 对象时创建。AsQueryable() 方法将创建一个可查询对象,该对象可以针对 Product 对象集合执行各种 LINQ 查询。WCF 服务会将 OData 概述中描述的 URL 查询转换为 LINQ 查询,并且此接口将返回符合条件的匹配结果。

在此示例中,供应商不是来自数据源中的供应商列表的简单转换。在转换为 IQueryable 之前,应用了一个过滤器,该过滤器仅公开未归档的供应商。如您所见,您可以在将原始集合返回给 WCF 服务之前对其应用任何操作。

第三个属性是派生自供应商列表以创建伦敦供应商。在这种情况下,创建了一个名为 LondonSuppliers 的新类,它代表了数据源中的一个新资源。 WCF 服务不关心您如何提供资源集,只要您将其作为 IQueryable 对象返回。而不是属性体,代码放在构造函数中,并通过 setter 分配给属性。

Productsupplier 类可以是普通类 - 您无需继承某些基类、设置属性等。您唯一可能想做的事情是设置哪个字段将用作键属性。否则,反射提供程序将使用以下规则选择其中一个:

  • 如果类有一个名为 ID 的属性,则它是具有 ID 作为唯一键属性的实体。
  • 如果类名为 Customer,并且它有一个名为 CustomerID 的属性,则该属性将用作键。

如果您的键属性不遵循此约定,您将需要指定数据键名称,如下面的列表所示:

[DataServiceKey("Name")]
public class LondonSupplier
{
    public int ID { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string Email { get; set; }
} 

在这里,我明确定义了 Name 属性将用作键,而不是 ID 属性。这段代码是创建功能齐全的 WCF 服务所需的一切。您可以使用任何方法来获取数据,而不仅仅是纯列表,然后在数据源提供程序中将其转换为 IQueryable

Entity Framework 数据源提供程序

创建数据源提供程序的最简单方法是创建 Entity Framework 模型并直接将其传递给 WCF 服务。Entity Framework 模型的示例显示在下图:

要公开 Entity Framework 模型中的实体,您只需将模型类放入服务声明中。

public class MyWcfDataService : DataService< MyEntityFrameworkModel >
{

}

实体框架模型中的每个实体都将被识别为一个可以公开的资源。这些实体同时实现 IQueryableIUpdateable 接口,因此实体可以被读取和更新。您需要做的就是像以前一样定义访问权限。访问权限定义的示例显示在以下代码中:

public static void InitializeService(DataServiceConfiguration config)
{
	config.SetEntitySetAccessRule("Companies", EntitySetRights.All);
	config.SetEntitySetAccessRule("Products", EntitySetRights.AllRead);
	config.SetEntitySetAccessRule("Categories", EntitySetRights.AllWrite);

	config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
}    

这就是实现 WCF Data Service 所需的全部内容。唯一需要注意的可能是命名约定。如果您在模型中复数和单数化实体,您将在模型图中看到单数形式的名称,但在配置中,您需要使用复数形式的名称。如果您遗漏了名称,服务将无法初始化。

如果您不想直接将 Entity Framework 模型绑定到服务,您可以创建一个自定义数据源提供程序,通过 IQueryable 属性公开实体。示例显示在以下列表中:

public class MyWCFDataSource
{
    public MyWCFDataSource()
    {
        var ctx = new OData.Models.ODataDemoEntities();

        this.MyLondonSuppliers = ctx.Companies.Select(c => new LondonSupplier()
        {
                ID = c.CompanyID,
                Name = c.Name,
                Address = c.Address
        });
    }

    public IQueryable<Supplier> MyLondonSuppliers 
    {
        get;
        private set;
    }
}  

此代码等同于上一节中的代码。而不是像前面的示例那样使用纯列表,我使用了从实体模型中获取的实体。如果您需要合并来自多个数据源的实体并将它们作为单个集合返回,则此方法很有用。

WCF 服务自定义

修改 WCF 服务有很多可能性。大多数设置都放在 WCF 服务的 InitializeService 方法中。您可以设置的一些属性是:

  • MaxResultSetsPerCollection – 定义一个查询中返回的最大对象数。
  • SetEntitySetPageSize(<entity-set>, limit) – 定义单个实体集的最大结果集。它不能与 MaxResultsSetPerCollection() 属性一起使用。
  • MaxExpandCount – 定义 $expand 参数中子资源的最大数量。例如,如果设置为 2,您将无法使用 URL 查询 /Products(1)?$expand=Supplier,Category,ProductItems,因为它需要三个展开的属性。
  • MaxExpandDepth – 定义 $expand 选项中路径的最大深度。例如,如果设置为 2,则无法运行 /Products(1)?$expand=Supplier/Category/Products 查询,因为它在路径中有三个级别。
  • DataServiceBehaviour.AcceptCountRequests – 定义是否允许使用 $count$inlinecount 的查询。默认值为 true
  • DataServiceBehaviour.AcceptProjectionRequests – 定义是否允许使用 $select 系统参数的查询。默认值为 true
  • DataServiceBehaviour.InvokeInterceptorsOnLinkDelete – 定义是否应在删除操作上执行更改拦截器。

这些属性在 WCF 服务的 InitializeService 方法中配置,例如:

public static void InitializeService(DataServiceConfiguration config)
{       
    config.MaxResultSetPerCollection = 100;
    config.MaxExpandDepth = 2;
    config.DataServiceBehaviour.AcceptCountRequests = false;          
}  

我在上面提到,如果您将查询发送到 Northwind OData 服务,它会将结果数量限制为 20,而不管您将 $top 选项设置为多少。在这里,您可以了解如何在服务器端覆盖此设置。

您可以设置在请求开始之前执行的事件处理程序,以及在发生错误时执行的事件处理程序。这两个服务方法都可以被覆盖,如下面的列表所示:

protected override void OnStartProcessingRequest(ProcessRequestArgs args)
{
    var method = args.OperationContext.RequestMethod;
    base.OnStartProcessingRequest(args);
}
protected override void HandleException(HandleExceptionArgs args)
{
    base.HandleException(args);
} 

每次调用服务时,它都会调用数据源类的构造函数。如果您想控制数据源的创建方式,可以在 CreateDataSource 方法中覆盖此行为,如下面的列表所示:

protected override MyWCFDataSource CreateDataSource()
{
    return base.CreateDataSource();
}

请注意,此方法在 OnStartProcessingRequest 之前被调用。

我在上面提到 WCF 服务并非 100% 兼容 OData 服务。一个区别是 WCF 服务不支持 $format$callback 选项,这在创建 JSON 客户端时会产生问题。默认情况下,您无法将 $format=json 传递给 WCF 服务 - 获取 JSON 数据的唯一方法是将 Http 请求头的 accept 参数设置为“application/json”。要解决此问题,您需要手动创建一个属性(我们称之为 JSONPSupportBehaviourAttribute),该属性会从请求中删除 $format 参数并设置请求头值。附加代码中已包含此类属性的示例。您需要做的就是在 WCF 服务中设置应能够返回 JSON 的属性,如下面的列表所示:

[JSONPSupportBehaviourAttribute]
public class MyWcfDataService : DataService< MyDataSourceClass >
{

} 

此修改使您能够使用 $format$callback 参数。

拦截器

您可以创建在资源读取或更新之前执行的方法。您需要做的就是在 WCF 服务中添加方法,并将其标记为 QueryInterceptorChangeInterceptor,针对某个资源集。

QueryInterceptors 返回应由所有返回结果满足的某个条件。通过修改此条件,您可以筛选当前查询返回的资源。以下列表中所示的方法拦截了对产品的读取请求:

[QueryInterceptor("MyProducts")]
public Expression<Func<Product, bool>> FilterProducts()
{
     if (HttpContext.Current.Session["UserID"] != null)
         return p => p.ModifiedBy == Convert.ToInt32(HttpContext.Current.Session["UserID"]);
     else
         return p => p.ModifiedBy == 0;
}

此拦截器放置在 WCF 服务中,它检查 UserID 是否在会话中。在这种情况下,它只返回属于当前用户的产品子集。否则,它返回“public”产品(ModifiedBy = 0 的那些)。

ChangeInterceptors 拦截 create/update/delete 请求,并允许您修改数据。以下列表中所示的拦截器会检查当前用户是否修改了产品,并将修改日期设置为当前日期:

[ChangeInterceptor("MyProducts")]
public void OnChangeProducts(Product product, UpdateOperations operations)
{
    if (Convert.ToInt32(HttpContext.Current.Session["UserID"]) != product.ModifiedBy)
        throw new Exception("Product cannot be updated");
    if (operations == UpdateOperations.Delete)
        throw new Exception("Product cannot be deleted");
    product.ModifiedOn = DateTime.Now;
}

拦截器使您能够对资源实现精细控制。在 InitializeService 中,您可以设置全局规则,定义客户端是否可以写入数据,或者是否可以读取所有记录。但是,您无法定义特定用户是否可以写入数据,或者某个用户是否应该只看到所有数据的子集。

使用拦截器,您可以添加自己的自定义逻辑,该逻辑可以检查访问权限并根据当前用户信息、Cookie 等信息执行一些自定义处理。这只是对某些访问规则逻辑的模拟,但在实际场景中,您可以轻松实现基于角色的安全、身份验证等。

服务操作

尽管 WCF Data Services 面向资源,但这并不意味着您不能添加自己的数据访问服务。在 WCF 服务中,您可以添加自己的操作来返回自定义资源集。这在您需要提供预编译的服务器端查询时可能很有帮助。

以下列表中所示的操作示例返回按城市划分的供应商列表:

[WebGet]
public IQueryable<Supplier> GetSuppliersByCity(string city)
{
     return DataRepository.Suppliers.Where(s => s.Town.Contains(city)).AsQueryable();
}

您可以以与其他任何资源相同的方式使用此服务操作 - 只需按名称调用它,提供参数,并添加您需要的任何服务参数。一些示例是:

  • /GetSuppliersByCity?city='Manchester'&$orderby=Name desc
  • /GetSuppliersByCity?city='Cardiff'&$top=1

您不需要在服务操作中使用任何参数。以下示例显示了如何使用服务操作实现伦敦供应商资源:

[WebGet]
public IQueryable<Supplier> LondonSuppliers()
{
      return DataRepository.Suppliers.Where(s => s.County == "London").AsQueryable();
}

这与先前使用 MyLondonSuppliers 的示例相同,其中相同的逻辑放在了数据源提供程序中。数据源中的资源与服务操作之间没有区别,因此您可以创建标准的 URL 查询,例如:

  • /LondonSuppliers?$skip=10&$take=5

您还可以创建不返回结果而只更新实体的服务操作。这种操作显示在以下列表中:

[WebGet]
public string UpdateProductName(int ID, string Name)
{
    DataRepository.Products.First(p => p.ProductID == ID).Name = Name;
    return "ok";
} 

这只是一个模拟,但在实际情况下,您可能会更新数据库信息。您可以使用以下 URL 调用此操作 /UpdateProductName?ID=2&Name='My Product',产品 ID 为 2 的产品将被更新。服务操作的响应将被包装在 XML 中,例如 <UpdateProductName>ok</UpdateProductName>。您可以将 [WebGet] 替换为 [WebInvoke] 属性,以禁止 GET 请求并仅允许通过 POSTPUT 或其他 Http 操作调用。

您需要添加的唯一内容是在 WCF 服务的 InitializeService 方法中设置服务操作的访问级别。示例显示在以下列表中:

public static void InitializeService(DataServiceConfiguration config)
{            
    config.SetServiceOperationAccessRule("GetSuppliersByCity", ServiceOperationRights.AllRead);
    config.SetServiceOperationAccessRule("LondonSuppliers", ServiceOperationRights.AllRead);
    config.SetServiceOperationAccessRule("UpdateProductName", ServiceOperationRights.All);

    config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;          
} 

在这里,我允许对 GetSupplierByCityLondonSuppliers 进行读取访问,并对 UpdateProductName 服务操作进行完全访问。

使用 OData 服务

现在我们已经看到了在哪里可以找到 public OData 服务以及如何构建自己的服务,我将向您展示如何使用这些服务。正如您已经看到的,OData 查询是通过 URL 执行的简单 Http 请求,因此您所需要的只是一个 Web 浏览器。然而,在实践中,您将使用其他工具来访问 OData 服务。

LINQPad

LINQPad 是一个优秀的工具,可以帮助您直接向 WCF 服务发送查询。

要连接到 WCF 服务,您需要最新版本的 LINQPad(至少是 4.0 版)。在那里,您可以看到“添加连接”链接,它允许您选择连接类型(选择 WCF Data Services (OData)),然后输入服务所在的 URI(例如,http://services.odata.org/OData/OData.svc/),如果服务不是公开的,可以选择输入用户名和密码,然后创建连接。如果成功,您将在左侧看到您添加的 OData 服务提供的所有资源。现在您可以创建一些 LINQ 查询并执行它。执行查询后,您可以看到结果或生成的 URL 查询。示例显示在下图:

LINQPad 是一个很好的工具,如果您想学习 OData 查询。只需输入任何您会使用的 LINQ 查询,看看生成的等效 URL 查询是什么。

C# 代码通过 Web 引用

WCF 服务可以像任何其他 Web 服务一样从 C# 代码中使用。只需右键单击解决方案,添加新的服务引用,然后输入 OData 服务的 URL,如下面的图所示:

如果 URL 正确,您将看到 OData 服务提供的所有资源(在上例中为 CategoriesProductsSuppliers)。现在,只需为服务引用键入命名空间,您就可以向服务发送查询了。

读取数据

从代码中查询 OData 服务很简单 - 只需使用 Uri 创建服务引用的实例并运行一些 LINQ 查询。示例显示在以下列表中:

var odataService = new DemoService(new Uri("http://services.odata.org/OData/OData.svc/"));
var products = odataService.Products.Where
               (p=>p.Name.Length<10).OrderBy(p=>p.Name).Skip(3).Take(4); 

但是,您需要意识到这只是 LINQ 到 OData URL 查询的翻译。因此,如果您创建不支持的 LINQ 查询,可能会遇到一些问题。假设您想检查是否存在 ID 为 430 的类别。您可能想找到这些类别的数量并检查计数是 0 还是 1,如下面的示例:

var count = (odataService.Categories.Where(c => c.ID == 430)).Count();

但是,如果您运行此查询,您将收到以下消息:“Unhandled Exception: System.NotSupportedException: Cannot specify query options (orderby, where, take, skip) on single resource.”问题在于翻译过程中识别出 c.ID 是一个标识符,因此发送的查询是 /Categories(430)。该查询返回单个实体,您不能对单个实体应用 Count()(尽管错误消息中未提及)。此外,CountFirstFirstOrDefault 不受支持,因此以下查询将以不支持的消息失败:

var count = odataService.Categories.Count(c => c.ID == 430);
var cat = odataService.Categories.FirstOrDefault(c => c.ID == 430);

然而,这个查询工作正常:

 var cat = odataService.Categories.Where(c => c.ID == 430).FirstOrDefault(); 

在此代码中,首先执行 LINQ 到 Http 查询,该查询将 Where 部分转换为 URL 查询。然后,在内存中的结果上应用 FirstOrDefault。正如您所见,您应该谨慎使用这些查询。请注意,这在我当前使用的服务库中会失败,因此将来版本可能已修复。如果您不确定 LINQ 查询是否受支持,最好先在 LINQPad 中尝试。

如果您的查询有效,您可以通过检查返回的对象轻松检查它,并找到将发送到 OData 服务器的查询。示例显示在下图:

在这里,我创建了一个跳过 2 并获取 3 个产品的 LINQ 查询。如果您在调试模式下检查返回的对象,您可能会看到已添加 $skip$top 参数。这类似于 LINQPad 的功能。

更新数据

除了查询之外,C# 代码在您需要通过服务更新数据时也非常理想。

在这种情况下,我将使用一个 public OData 服务的可写版本来向您展示如何进行更新。读写演示 OData 服务允许您更新数据,但仅限于您的会话。每个会话都有一个唯一的 URL,格式为 http://services.odata.org/(S(SESSION_ID))/OData/OData.svc/。在此示例中,我将随机选择会话 d21qfurvulln1hs3cjn5Tiur 作为会话 ID,因此我将用于创建服务的 URL 将类似于 http://services.odata.org/(S(d21qfurvulln1hs3cjn5Tiur))/OData/OData.svc/。在下面的代码中,您可以看到如何实现更新:

var odataService = new DemoService(
new Uri("http://services.odata.org/%28S%28d21qfurvulln1hs3cjn5Tiur%29%29/OData/OData.svc/"));
           
var music = new Category() { ID = 430, Name = "Music"};
foreach (var p in odataService.Products.Take(2))
    music.Products.Add(p);
odataService.AddToCategories(music);
odataService.SaveChanges();

var cat = odataService.Categories.Where(c=>c.ID == 430).First();

foreach (var p in cat.Products)
{
    Console.WriteLine(p.Name + "(" + p.ID + ")");
}

odataService.DeleteObject(cat);
odataService.SaveChanges();

在这里,我创建了 Demo 服务的引用,创建了一个新类别,获取了两个产品并将它们关联到该类别,保存了更改,再次获取了类别,列出了产品并删除了类别。正如您所看到的,OData 上的 LINQ 操作与在 Entity Framework 上应用的标准 LINQ 操作相同。

JavaScript 代码示例

OData 服务通过 URL 调用,并可以返回 JSON,因此很容易通过 JavaScript AJAX 调用。以下示例显示了如何使用 JQuery 调用发送 AJAX 请求:

$.ajax({
    url: "WcfDataService2.svc/MySuppliers",
    dataType: "json",
    cache: "false",
    success: function (response) {
        alert("Returned " + response.d.length + " suppliers")
    }
}) 

FireBug 中跟踪的响应示例显示在下图:

您可以使用此 JSON 对象并通过 JavaScript 直接显示任何数据。JSON 输出格式非常适合 JavaScript 代码,因为您可以在那里使用 JS 模板引擎,它使用模板将 JSON 数据转换为 HTML 输出,如下面的图所示:

在这种情况下,您需要准备一些 HTML 模板,当您从服务加载 JSON 时,您可以将其加载到模板中并生成实际的 HTML。以下列表显示了使用 loadJSON 模板引擎的示例:

$.ajax({
    url: "WcfDataService2.svc/MySuppliers",
    dataType: "json",
    cache: "false",
    success: function (response) {
        $("#template").loadJSON(response.d);
    }
})  

模板引擎的用法细节超出了本文的范围,但您可以在 ASP.NET MVC 中的 jQuery 模板/视图引擎 文章中找到更多详细信息,其中我展示了如何在返回 JSON 响应的 MVC 应用程序中使用各种模板引擎。在这种情况下,您有一个返回 JSON 格式的 OData 服务,而不是返回 JSON 的控制器。

其他工具/库

有很多应用程序可以消耗 OData 服务 - 您可以在 OData 网站 上找到完整列表。此外,您还可以找到许多可以帮助您从 Java、JavaScript、PHP 等使用 OData 服务的库 - 库的完整列表列在 OData 网站 上。

结论

OData 服务是实现 Web 服务的绝佳方式。与公开特定方法的传统方式不同,这种方法公开了整个实体,并允许您的客户端创建他们需要的任何查询。如果您从 .NET 客户端使用它,您就可以使用标准的 LINQ 进行查询 - 因此访问更简单。此外,您还可以控制访问、自定义查询以及所有您在标准 Web 服务中拥有的内容,因此您不会丢失任何东西,但您可以创建更灵活的 Web 服务。对我来说,与标准 Web 服务相比,OData Web 服务始终是更好的选择,因此我建议您尝试使用它们。

历史

  • 2012 年 6 月 1 日:初始版本
© . All rights reserved.