数据点

WCF 服务中的 LINQ 投影查询和替代方案

Julie Lerman

下载示例代码

Julie Lerman上个月当我的本地 .NET 用户组的演示者正在课堂上写 LINQ 查询时,我问他,“以前没有 LINQ 的时候,我们是怎么过的”?他回答说,“真是难以想象”。

这是真的。自从 2008 年 LINQ 被引入 Visual Studio 后,它对我们在 Microsoft .NET Framework 中的编程方式产生了如此 重大的影响。与 Visual Basic 和 C# 中引入的许多新的语言功能相结合,LINQ 可以前后一致地解决查询内存中对象和数据来源的问题。

LINQ 具备将形状随机的数据投影到匿名类型的功能,这种功能既有好的一面,也有不好的一面。如果您只需要获取数据的特定视图,而不必为此一次性的类型声明新类,匿名类型是一个不错的解决方案。LINQ 的投影和匿名类型的确是把我们宠坏了。那么,为什么我说,它们也有不好的一面呢?

如果您曾经将 LINQ 投影用于需要将数据返回另一方法的某种方法,或者更糟糕,将 LINQ 投影用在 Windows Communication Foundation (WCF) 服务操作中,对此您可能会有所了解。

原因即在于匿名类型是一次性类型,它们没有声明,只有创建它们的方法可以理解它们。如果您写了一个返回一列匿名类型的查询,因为没有办法表达“匿名类型”,所以没有办法定义某个方法参数,说:“我将返回一列...”。

以下是一个采用简单投影的 LINQ to Entities 查询:

var custQuery = from c in context.Customers

                 select new {c.CustomerID, Name=c.LastName.Trim() + 

                 ", " + c.FirstName};

在运行时,custQuery 变量将实际成为某个 ObjectQuery<<>f__AnonymousType0<int,string>>。

有了 var(以及 Visual Basic Dim 的替代使用)我们不再需要找到这种非类型的表达方式。

如果您想从某个方法返回该查询的结果,唯一合理的解决方案是创建代表要返回的类型的类。不过,这样做,提供匿名类型将毫无意义。现在您必须写更多的代码,定义类,还可能需要定义容纳新类的新项目,并确保使用这些类的程序集能访问到它们等等。

最近,数据服务又给出了一道难题。为了对数据进行投影,您必须在服务中创建自定义的操作,执行自己的查询,然后返回某一预先定义的、可以为客户端理解的类。

在您处理服务时,很多情况下您都希望在无需通过线路移动大规模类型的情况下,处理数据的特定视图。

为了满足这一临时性要求,除了在您的域中创建额外的类型之外,您还有更多的选择。

WCF 数据服务中的新投影功能

.NET Framework 3.5 SP1 的数据服务更新为 WCF 数据服务引入了少数几个强大的功能,这也是 .NET Framework 4 的组成部分。这些功能中就有针对数据服务在查询中使用投影的功能。强烈建议您查看 WCF 数据服务团队的博客帖子 (blogs.msdn.com/astoriateam/archive/2010/01/27/data-services-update-for-net-3-5-sp1-available-for-download.aspx),以了解这次更新的所有新功能。

数据服务 URI 语法中添加了 $select 运算符。它允许使用属性甚至是导航属性投影。

下面简单举例说明了随 SalesOrderHeaders 导航属性获取客户的几个标量属性的投影:

http://localhost /DataService.svc/Customers(609)
  $select=CustomerID,LastName,FirstName,SalesOrderHeaders&$expand=
  SalesOrderHeaders

扩展运算符强制结果不仅包含到这些订单的链接,还包含每个订单的数据。

图 1 显示了此查询的结果。扩展的 SalesOrderHeaders(只包含一个订单)以黄色突出显示,而客户信息以绿色突出显示。

Figure 1 Results of a Data Services Query Projection Requesting Three Customer Properties and the Customer’s SalesOrder-Headers
图 1 请求三个客户属性和客户的 SalesOrderHeaders 的数据服务查询投影的结果

.NET Framework 中的 LINQ to REST 功能以及 WCF 数据服务的 Silverlight 客户端 API 已得到了更新,也允许使用投影:

var projectedCust = (from c in context.Customers

                    where c.CustomerID==609

                    select new {c.CustomerID, c.LastName})

                    .FirstOrDefault();

ProjectedCust 现在是一个可供用于客户端应用程序的匿名类型。

它还可能投影到已知实体类型,而且在某些情况下,DataContext 可以跟踪由客户端所做的更改,这些更改还可以通过服务的 SaveChanges 方法保留下来。请注意,任何缺少的属性将被填充其默认值(或填充为空,如果它们可为空值),并保留到数据库。

自 EDM 启用投影强类型

如果您使用实体框架实体数据模型 (EDM),可使用某种方便易用的方法,在您需要从创建匿名类型的方法中将它们传递出时避免被卡住。

EDM 有名为 QueryView 的映射。我过去在提供数据服务投影支持前,曾向很多客户指明这一点。它不仅可以为数据服务很好地解决这个问题,还能为自定义 WCF 服务和 RIA 服务解决问题。

什么是 QueryView?这是实体框架元数据中的一种特殊映射类型。如图 2 所示,通常,您会根据元数据的存储模型存储架构定义语言 (SSDL) 的说明,将实体的属性映射到数据库表或视图列。

Figure 2 Mapping Table Columns Directly to Entity Properties
图 2 直接将表列映射到实体属性

与此相反,QueryView 会让您通过这些 SSDL 表列创建视图,而不是直接映射到它们。使用 QueryView 有多个理由。一些例子包括:采用只读方式公开实体,以条件映射不允许的方式过滤实体,或提供数据库中数据表的不同视图。

针对上述目的的最后一个,我将重点关注您经常会在自己的应用程序中投影的匿名类型的替代方案。参数选用表即是其中一个例子。为什么要为只需要 ID 和客户姓名的下拉菜单返回全部客户类型?

生成 QueryView

创建 QueryView 之前,您需要在模型中创建代表您所针对的视图形状的实体,如 CustomerNameAndID 实体。

但是您不能将这个实体直接映射到 SSDL 中的 Customer 表。将 Customer 实体和 CustomerNameAndID 实体都映射到表的 CustomerID 列会产生冲突。

正如您可以在数据库中创建表视图,您可以改为直接在元数据中创建 SSDL Customer 视图。QueryView 就是 SSDL 上的 Entity SQL 表达式。它是模型的映射规范语言 (MSL) 元数据的一部分。创建 QueryView 时无法获得设计器支持,所以您需要直接在 XML 中键入。

因为您要映射到表的存储架构,所以最好看看其外观。图 3 列出了 Customer 数据库表的 SSDL 描述,除了使用了提供商数据类型之外,它与概念模型的元数据的 Customer 实体类似。

图 3 数据库 Customer 表的 SSDL 描述

<EntityType Name="Customer">

  <Key>

    <PropertyRef Name="CustomerID" />

  </Key>

  <Property Name="CustomerID" Type="int" Nullable="false"

            StoreGeneratedPattern="Identity" />

  <Property Name="Title" Type="nvarchar" MaxLength="8" />

  <Property Name="FirstName" Type="nvarchar" Nullable="false" 

            MaxLength="50" />

  <Property Name="MiddleName" Type="nvarchar" MaxLength="50" />

  <Property Name="LastName" Type="nvarchar" Nullable="false" 

            MaxLength="50" />

  <Property Name="Suffix" Type="nvarchar" MaxLength="10" />

  <Property Name="CompanyName" Type="nvarchar" MaxLength="128" />

  <Property Name="SalesPerson" Type="nvarchar" MaxLength="256" />

  <Property Name="EmailAddress" Type="nvarchar" MaxLength="50" />

  <Property Name="Phone" Type="nvarchar" MaxLength="25" />

  <Property Name="ModifiedDate" Type="datetime" Nullable="false" />

  <Property Name="TimeStamp" Type="timestamp" Nullable="false"

            StoreGeneratedPattern="Computed" />

</EntityType>

存储架构的命名空间 ModelStoreContainer 是 QueryView 的另一个重要元素。现在,您已经拥有了构建 QueryView 表达式的所有必要元件。以下 QueryView 将三个必填字段从 SSDL 投影到我在模型中创建的 CustomerNameAndID 实体:

SELECT VALUE AWModel.CustomerNameAndID(c.CustomerID, c.FirstName, 

        c.LastName) FROM ModelStoreContainer.Customer as c

我们来描述下该实体 SQL:“查询存储架构中的客户,取出这三列,让他们作为 CustomerNameAndID 实体返回给我。”AWModel 是概念模型的实体容器的命名空间。对于表达式中引用的概念架构定义语言 (CSDL) 和 SSDL 类型,您需要使用其强类型化名称。

只要投影的结果(整数、字符串与字符串)与目标实体的架构相匹配,映射将成功。我试过在投影内使用函数和连接(如 (c.CustomerID, c.FirstName + c.LastName)),但没有成功,并收到了不允许使用这些函数的错误消息。因此,我被迫使用 FirstName 和 LastName 属性,而让客户端处理连接问题。

将 QueryView 置入元数据

对于进入 EntityContainerMapping 的实体,您必须将 EntitySetMapping 元素内的 QueryView 表达式置入元数据。图 4 在我的 EDMX 文件的原始 XML 中显示了这一 QueryView(以黄色突出显示)。

Figure 4 A QueryView in the Mappings Section

图 4 映射部分的 QueryView

现在我的 CustomerNameAndID 已成为我的模型的一部分,可提供给任何消费者。此 QueryView 还有另一优势。尽管这一 QueryView 的目标是创建只读引用列表,您还可以使用 QueryView 更新所映射的实体。上下文将跟踪 CustomerNameAndID 对象的变化。虽然实体框架不能够为这一实体自动生成插入、更新和删除命令,您可以将存储过程映射到它。

受惠于 QueryView

既然您已经在模型中加入了 QueryView,您就不再需要依靠投影或匿名类型来检索数据的这些视图。如下所示,在 WCF Data Services 中,CustomerNameAndIDs 将成为可供查询的有效实体集:

List<CustomerNameAndID> custPickList = 

  context.CustomerNameAndIDs.ToList();

再没有杂乱的投影了;更有优势的一点是,您不需要在应用程序中定义新的类型,并投影到它们,即可在自定义 WCF 服务中创建现在可以返回此强类型化对象的服务操作。

public List<CustomerNameAndID> GetCustomerPickList()

    {

      using (var context = new AWEntities())

      {

        return context.CustomerNameAndIDs.OrderBy(

          c => c.LastName).ToList();

      }

    }

因受到限制,我们无法连接 QueryView 中的名字和姓氏,要由使用此服务的开发人员一方实现这个连接。

WCF RIA 服务也可受益于 QueryView。您可能希望公开某种方法,从您的域服务中检索某家餐厅的参数选用表。您不必在域服务中创建额外的类来表示这些投影的属性,该 RestaurantPickList 实体可获得模型中的 QueryView 支持,轻松提供这一数据:

public IQueryable<RestaurantPickList> GetRestaurantPickList()

    {

      return context.RestaurantPickLists;

    }

是选择 QueryViews 还是投影,我们已经算面面俱到了

有能力通过您的数据类型对视图进行投影是查询方面的巨大优势,这是 WCF 数据服务增加的一项很不错的功能。即便如此,如果有时不需要进行投影,也不必担心结果共享,即可访问这些视图,将简化您的某些编码任务。

最后一个注意事项:随着在实体框架的 .NET Framework 4 版本中引入外键,因为您可以返回只读实体,并轻松使用它们的属性来更新正在编辑的实体中的外键属性,QueryView 参数选用表将更有意义。  

Julie Lerman* 是 Microsoft MVP、.NET 导师和顾问,住在佛蒙特州的山区。您可以在全球的用户组和会议中看到她对数据访问和其他 Microsoft .NET 主题的演示。Lerman 是《Programming Entity Framework》(O’Reilly Media,2009)一书的作者,该书受到广泛称赞,她的博客地址是 thedatafarm.com/blog。请关注她的 Twitter:julielerman。*

衷心感谢以下技术专家审阅本文:Alex James