使用 EF4 构建 N 层应用程序

Daniel Simmons

下载代码示例

本文是介绍使用实体框架进行 n 层编程的系列文章中的第三篇(请参见 msdn.microsoft.com/magazine/dd882522.aspxmsdn.microsoft.com/magazine/ee321569.aspx),着重介绍了如何使用实体框架 (EF) 和 Windows Communication Foundation (WCF) 构建自定义 Web 服务(在某些情况下,基于 REST 的服务或其他方法更加合适,但在这些文章中,我重点介绍自定义 Web 服务)。第一篇文章介绍了一些重要的设计注意事项和反模式。在第二篇文章中,我介绍了可在 n 层应用程序中成功使用的四种模式。其中还包括一些代码示例,演示了可用来实现所谓简单实体模式的第一版实体框架 (EF 3.5 SP1)。在本文中,我将介绍第二版实体框架 (EF4) 提供的一些功能以及如何使用这些功能来实现“自我跟踪实体”和数据传输对象 (DTO) n 层模式。

尽管简单实体通常不是 n 层应用程序的首选模式,但它是第一版 EF 中最可行的选项。但是,为了使用框架进行 n 层编程,EF4 对选项进行了重要更改。其中一些重要的新增功能包括:

  1. 支持在连接断开时进行操作的新框架方法,如将实体或关系更改为新状态(例如,已添加或已修改)的 ChangeObjectState 和 ChangeRelationshipState、允许您为实体设置原始值的 ApplyOriginalValues 以及框架一创建实体便会引发的新 ObjectMaterialized 事件。
  2. 支持在实体中使用 Plain Old CLR Objects (POCO) 和外键值。通过这些功能可以创建能够在中间层服务实现与其他层服务实现之间共享的实体类,这些服务实现可能具有不同的实体框架版本(例如,.NET 2.0 或 Silverlight)。包含外键的 POCO 对象还具有简单的序列化格式,从而可简化与 Java 等平台之间的互操作性。使用外键还可为关系启用简单得多的并发模型。
  3. 用于自定义代码生成的 T4 模板。使用这些模板可以生成用于实现“自我跟踪实体”或 DTO 模式的类。

实体框架团队已在模板中使用这些功能来实现“自我跟踪实体”模式,从而使该模式更易于访问,但 DTO 仍然需要在初始实现期间开展大量工作,EF4 的使用使这一过程也可得到简化(“自我跟踪实体”模板和其他一些 EF 功能作为 Web 下载功能社区技术预览 (CTP) 的一部分提供,而不是包含在 Visual Studio 2010/.NET 4 包中。本文中的示例假定 Visual Studio 2010/.NET 4 和 CTP 功能均已安装)。借助这些新功能,可根据体系结构的优点(关注点分离/松散耦合、约定的强度、有效的传输格式和互操作性)、是否易于实现和上市时间之间的权衡来评估我所介绍的四种模式(简单实体、变更集、自我跟踪实体和 DTO)。如果在表示此权衡的图中绘制这四种模式,结果可能类似于图 1


图 1 使用 EF4 的 N 层模式比较

在特定情况下哪种模式更为合适取决于许多因素。通常,DTO 在体系结构上有很多优势,但需要以较高的初始实现成本作为代价。变更集在体系结构方面几乎体现不出任何优势,但它易于实现(当它可用于特定技术,例如传统 ADO.NET 中的 DataSet 时)。

为了在这些因素之间实现实用而又灵活的平衡,我建议您从“自我跟踪实体”开始,然后在情况允许时再改用 DTO。通常,您可以迅速掌握“自我跟踪实体”,并且仍然可以实现在体系结构方面的许多重要目标。与“变更集”或“简单实体”相比,此方法代表着更佳的权衡,只有在您没有其他可行选择时,我才建议您使用“变更集”或“简单实体”。另一方面,当您的应用程序变得更大、更复杂时,或者“自我跟踪实体”无法满足您的要求时(如客户端和服务器具有不同的更改频率),DTO 无疑成为最佳选择。这两种模式是您的工具箱中最为重要的工具,所以这里将分别进行介绍。

自我跟踪实体

若要将此模式用于实体框架,请先创建一个代表概念实体的实体数据模型,然后将其映射到一个数据库。您可以根据现有数据库反向设计模型并对其进行自定义,也可以从头开始创建模型,然后生成与之匹配的数据库(EF4 中的另一项新功能)。此模型和映射就位后,可用“自我跟踪实体”模板替换默认的代码生成模板,具体方法是右键单击实体设计器图面并选择“添加代码生成项”。

接下来,请从已安装模板的列表中选择“自我跟踪实体”模板。此步骤将关闭默认的代码生成,并向您的项目中添加两个模板:一个模板用于生成 ObjectContext,另一个模板用于生成实体类。通过使用两个模板进行代码生成,可以将代码拆分为相互独立的程序集:一个用于实体类,一个用于上下文。

这种方法的主要优点在于,您可将实体类置于一个与实体框架不存在依赖关系的程序集中。这样,如果您需要,中间层和客户端便可共享实体程序集(或者至少是它所生成的代码)和您已实现的任何业务逻辑。上下文将保留在与实体和 EF 均存在依赖关系的程序集中。如果服务的客户端运行的是 .NET 4,则可从客户端项目中直接引用实体程序集。如果客户端运行的是 .NET 的早期版本或 Silverlight,则可能要添加从客户端项目到所生成文件的链接,并在该项目中重新编译实体源代码(指向相应的 CLR)。

无论项目结构如何,这两个模板均可协作实现“自我跟踪实体”模式。所生成的实体类是简单的 POCO 类,这些类除了存储实体属性的基本功能之外只有另外一项功能,即跟踪对实体的更改:实体的总体状态、对关键属性(如并发令牌)的更改以及对实体间关系的更改。这种额外的跟踪信息属于实体 DataContract 定义的一部分(所以,当您向 WCF 服务发送实体或接收来自 WCF 服务的实体时,会附带相应的跟踪信息)。

在服务的客户端上,即使未将实体附加到任何上下文,也会自动跟踪对实体的更改。所生成的每个实体针对每个属性都有类似如下的代码。如果您更改某一实体中状态为 Unchanged 的属性值,例如,将该状态更改为 Modified:

[DataMember]
public string ContactName
{
    get { return _contactName; }
    set
    {
            if (!Equals(_contactName, value))
            {
                _contactName = value;
                OnPropertyChanged("ContactName");
            }
    }
}
private string _contactName;

同理,如果向图表中添加新实体或从图表中删除实体,也会跟踪该信息。由于每个实体的状态都在该实体自身中进行跟踪,因此即使将从多个服务调用中检索的实体相关,该跟踪机制仍可按预期方式工作。如果建立了新关系,将只跟踪该更改 — 所涉及的实体将保留原状态,就像这些实体是从同一个服务调用中检索到的一样。

上下文模板会向生成的上下文中添加新方法 ApplyChanges。ApplyChanges 会向该上下文中附加一个实体图表,并在 ObjectStateManager 中设置信息,以与实体的跟踪信息相匹配。生成的代码将借助实体对自身的跟踪信息和 ApplyChanges 处理更改跟踪和并发问题,这是正确实现 n 层解决方案最困难的两个部分。

作为一个具体示例,图 2 演示了一个简单的 ServiceContract,可将其与“自我跟踪实体”一起使用,基于 Northwind 示例数据库来创建 n 层订单提交系统。

图 2 自我跟踪实体模式的简单服务约定

[ServiceContract]
public interface INorthwindSTEService
{
    [OperationContract]
    IEnumerable<Product> GetProducts();

    [OperationContract]
    Customer GetCustomer(string id);

    [OperationContract]
    bool SubmitOrder(Order order);

    [OperationContract]
    bool UpdateProduct(Product product);
}

GetProducts 服务方法用于在客户端中检索有关产品目录的参考数据。此信息通常在本地缓存,不常在客户端中更新。GetCustomer 可检索客户以及该客户的订单列表。该方法的实现非常简单,如下所示:

public Customer GetCustomer(string id)
{
    using (var ctx = new NorthwindEntities())
    {
        return ctx.Customers.Include("Orders")
        .Where(c => c.CustomerID == id)
        .SingleOrDefault();
    }
}

此代码与使用“简单实体”模式时实现此类方法的代码基本相同。差别在于返回的实体将进行自我跟踪,这意味着使用这些方法的客户端代码也非常简单,但它可以完成更多任务。

为了便于说明,我们假定在订单提交过程中,您不仅要创建具有适当订单详细信息行的订单,还要用最新的联系人信息更新客户实体的部分内容。而且,您要删除 OrderDate 为 null 的所有订单(系统可能用此方法标记被拒绝的订单)。对于“简单实体”模式,将实体的添加、修改和删除操作组合在一个图表中将需要对每种操作类型进行多次服务调用;如果您尝试在 EF 第一版中实现“自我跟踪实体”之类的任务,将需要非常复杂的自定义约定和服务实现。使用 EF4 时,客户端代码可能类似于图 3

图 3 自我跟踪实体模式的客户端代码

var svc = new ChannelFactory<INorthwindSTEService>(
    "INorthwindSTEService")
    .CreateChannel();

var products = new List<Product>(svc.GetProducts());
var customer = svc.GetCustomer("ALFKI");

customer.ContactName = "Bill Gates";

foreach (var order in customer.Orders
    .Where(o => o.OrderDate == null).ToList())
{
    customer.Orders.Remove(order);
}

var newOrder = new Order();
newOrder.Order_Details.Add(new Order_Detail()
    {
        ProductID = products.Where(p => p.ProductName == "Chai")
                    .Single().ProductID,
        Quantity = 1
    });
customer.Orders.Add(newOrder);

var success = svc.SubmitOrder(newOrder);

此代码将创建服务,对该服务调用前两个方法以获取产品列表和客户实体,然后对客户实体图表进行更改,更改时使用构建直接与数据库对话的两层实体框架应用程序或在中间层实现服务时的同类代码。(如果您对创建 WCF 服务客户端的这种方式并不熟悉,则会自动为您创建客户端代理,而不为实体创建代理,原因是我们在重用自我跟踪实体模板中的实体类。如果需要,您也可以使用在 Visual Studio 中通过“添加服务引用”命令生成的客户端。但此处未涉及 ObjectContext。您只是在对实体自身进行操作。最后,客户端将调用 SubmitOrder 服务方法,将更改推送到中间层。 

当然,在实际应用程序中,客户端对图表的更改可能来自某种 UI,您将围绕服务调用添加异常处理(这在通过网络进行通信时尤其重要),但图 3 中的代码演示了其中的原理。需要注意的另一个重要事项是:在为新订单创建订单详细信息实体时,您仅设置 ProductID 属性,而非产品实体本身。这是使用中的新外键关系功能。由于只将 ProductID 通过序列化返回到中间层,而不序列化产品实体副本,因此该功能可减少通过网络传输的信息量。

SubmitOrder 服务方法的实现是“自我跟踪实体”真正的亮点所在:

public bool SubmitOrder(Order newOrder)
{
    using (var ctx = new NorthwindEntities())
    {
        ctx.Orders.ApplyChanges(newOrder);
        ValidateNewOrderSubmission(ctx, newOrder);
        return ctx.SaveChanges() > 0;
    }
}

对 ApplyChanges 的调用将完成所有工作。它将从实体中读取更改信息,并以适当方式将其应用于上下文中,以使得到的结果就像一直在对附加到上下文的实体执行更改一样。

验证更改

在 SubmitOrder 实现中还应注意对 ValidateNewOrderSubmission 的调用。我将此方法添加到了服务实现中,它可以检查 ObjectStateManager,以确保对 SubmitOrder 的调用中只存在我们需要的更改类别。

由于 ApplyChanges 会将它在整个相关对象图表中发现的所有更改都推送到上下文中,因此这一步骤非常重要。我们期望客户端只执行添加新订单和更新客户等操作,但这并不意味着客户端不会由于错误(甚至出于恶意)而执行其他操作。如果客户端更改了产品价格使订单比应有价格更便宜或更昂贵,该怎么办?在将更改保存到数据库之前总应对更改进行验证,这一关键规则比验证执行细节更加重要。无论您使用哪种 n 层模式,此规则都适用。

 另一个关键设计原则是应为每种操作开发独立、特定的服务方法。没有这些互相独立的操作,在两层之间就没有表示允许和不允许操作的可靠约定,也无法正确验证所做的更改。如果您使用单个 SaveEntities 服务方法而不是 SubmitOrder 和单独的 UpdateProduct 方法(只能由获权修改产品目录的用户访问),虽可轻松实现该方法的应用和保存部分,但由于您无法了解允许和不允许产品更新的时间,因此无法正确进行验证。

数据传输对象

“自我跟踪实体”模式可简化 n 层过程,如果您创建特定的服务方法并对每种方法进行验证,该过程的架构会十分合理。即使是这样,在该模式下执行的操作也会受限。当您遇到这些限制时,使用 DTO 可以摆脱困境。

在 DTO 中,不是在中间层与客户端之间共享单个实体实现,而是创建一个仅用于通过服务传输数据的自定义对象,并为中间层和客户端开发单独的实体实现。这种变化可提供两点优势:它可将服务约定与中间层和客户端上的实现问题相隔离,这样即使层中的实现发生更改约定也可以保持稳定,您还可以控制通过网络传输的数据流。因此,您无需发送不必要的数据(或不允许客户端访问的数据),也无需为服务之便而重塑数据。通常,在设计服务约定时会考虑客户端应用场景,以便在中间层实体与 DTO 之间重塑数据(可能通过将多个实体组合到一个 DTO 中并跳过客户端中不需要的属性),而 DTO 可直接在客户端上使用。

但这些优势的代价是必须多创建并维护一个或两个对象和映射层。为了扩展订单提交示例,可以创建一个专门用来提交新订单的类。此类会将客户实体的属性与新订单方案中所设置订单的属性组合起来,但将忽略两个实体中在中间层计算或在过程的其他阶段中设置的属性。这样可使 DTO 尽量小而高效。该实现应如下所示:

public class NewOrderDTO
{
    public string CustomerID { get; set; }
    public string ContactName { get; set; }
    public byte[] CustomerVersion { get; set; }
    public List<NewOrderLine> Lines { get; set; }
}

public class NewOrderLine
{
    public int ProductID { get; set; }
    public short Quantity { get; set; }
}

好的,这里实际上有两个类,一个用于订单,一个用于订单详细信息行,但数据量尽可能小。该代码中唯一看似无关的信息是 CustomerVersion 字段,其中包含用于对客户实体进行并发检查的行版本信息。由于数据库中已存在客户实体,因此需要将此信息用于该实体。订单和详细信息行是提交到数据库中的新实体,因此不需要其版本信息和 OrderID,版本信息和 OrderID 由数据库在保存更改时生成。

接受此 DTO 的服务方法所用的低级别实体框架 API 与“自我跟踪实体”模板完成其任务时使用的低级别实体框架 API 相同,但现在您需要直接调用这些 API,而不是通过生成的代码调用。该实现分为两个部分。首先,根据 DTO 中的信息创建由客户、订单和订单详细信息实体构成的图表(请参见图 4)。

图 4 创建实体图表

var customer = new Customer
    {
        CustomerID = newOrderDTO.CustomerID,
        ContactName = newOrderDTO.ContactName,
        Version = newOrderDTO.CustomerVersion,
    };

var order = new Order
    {
        Customer = customer,
    };

foreach (var line in newOrderDTO.Lines)
{
    order.Order_Details.Add(new Order_Detail
        {
            ProductID = line.ProductID,
            Quantity = line.Quantity,
        });
}

然后,将该图表附加到上下文并设置适当的状态信息:

ctx.Customers.Attach(customer);
var customerEntry = ctx.ObjectStateManager.GetObjectStateEntry(customer);
customerEntry.SetModified();
customerEntry.SetModifiedProperty("ContactName");

ctx.ObjectStateManager.ChangeObjectState(order, EntityState.Added);
foreach (var order_detail in order.Order_Details)
{
    ctx.ObjectStateManager.ChangeObjectState(order_detail, 
       EntityState.Added);
}

return ctx.SaveChanges() > 0;

第一行将整个图表附加到上下文,但执行此操作时,每个实体都处于 Unchanged 状态,因此,您首先告知 ObjectStateManager 将客户实体置于 Modified 状态,但只有 ContactName 属性标记为已修改。这一点非常重要,因为您实际上并没有全部客户信息,而只有 DTO 中的信息。如果将所有属性都标记为已修改,实体框架会尝试将一批 null 值和零值保存到客户实体中的其他字段。

接下来,您将订单及其每个订单详细信息的状态都更改为 Added,然后调用 SaveChanges。

那么,验证代码在哪儿?在本例中,有一个特定 DTO 专门用于您的应用场景,而且您在从该对象向实体中映射信息时是在解释该对象,因此在整个过程中都在执行验证。由于接触不到产品实体,所以此代码绝对不会意外更改产品价格。这是 DTO 模式只能间接发挥作用的优点。您仍然必须执行验证工作;该模式只强制执行一个级别的验证。在许多情况下,您的代码中需要包括有关值或其他业务规则的附加验证。

另一个注意事项是正确处理并发异常。如前所述,DTO 中包含客户实体的版本信息,所以当别人修改同一客户时您有能力正确检测并发问题。要使示例更加完整,应将此异常映射为 WCF 故障以便客户端解决冲突;或者,可以捕获该异常并应用某种自动策略以处理冲突。

如果要通过添加其他操作(如修改订单的功能)进一步扩展该示例,您可以创建另一个专门用于该应用场景的 DTO,其中只包含该应用场景的适当信息。此对象有些类似于 NewOrderDTO,但它具有订单和订单详细信息实体的 OrderID 和 Version 属性以及要允许服务调用更新的每个属性。该服务方法实现也与前面显示的 SubmitOrderDTO 方法相似:它将遍历 DTO 数据、创建对应的实体对象,然后在状态管理器中设置这些对象的状态,再将更改保存到数据库中。

如果要同时使用“自我跟踪实体”和“数据传输对象”来实现订单更新方法,您将发现“自我跟踪实体”实现会重用实体,并与新的订单提交方法共享几乎所有相同的服务实现代码 — 唯一的差别在于验证代码,但即使是验证代码也可能共享一部分。但是,DTO 实现要求两种服务方法分别使用不同的数据传输对象类,方法实现遵循类似的模式,但可以共享的代码微乎其微(如果有)。

来自 Trenches 的提示

下面是一些需要注意和了解的提示。

  • 务必在您的客户端重用“自我跟踪实体”模板生成的实体代码。如果您使用由 Visual Studio 中的“添加服务引用”或由其他工具生成的代理代码,那么大部分工作都看似正常,但您会发现实体实际上不会跟踪其在客户端上的更改。
  • 应在每个服务方法的 Using 语句中新建一个 ObjectContext 实例,以便在该方法返回前释放该实例。此步骤对于服务的可扩展性至关重要。它可确保数据库连接在服务调用之间不保持打开状态,特定操作使用的临时状态将在操作结束时进行垃圾回收。实体框架会自动在应用程序域中缓存元数据和它需要的其他信息以及 ADO.NET 池数据库连接,因此每次重新创建上下文的操作都很迅速。
  • 应尽可能使用新的外键关系功能。该功能可以大大简化更改实体间关系的操作。借助独立关联(在实体框架第一版中唯一可用的关系类型),可使关系的并发检查独立于实体的并发检查来执行,并且无法回避这些关系并发检查。因此,您的服务必须携带关系的原始值并在上下文中设置这些值,然后才能更改关系。但使用外键关系后,关系只是实体的一个属性,如果实体通过了并发检查,则无需再执行其他检查。仅通过更改外键值便可更改关系。
  • 在向 ObjectContext 附加图表时请注意 EntityKey 冲突。例如,如果您在使用 DTO,并且图表的某些部分表示新添加的实体,而由于将在数据库中生成这些实体的键值所以尚未设置这些值,那么您应该先通过调用 AddObject 方法来添加整个实体图表,再将状态不是 Added 的实体更改为所需状态,而不是先调用 Attach 方法再将状态为 Added 的实体更改为该状态。否则,当您最初调用 Attach 时,实体框架会认为应将每个实体都置于 Unchanged 状态,这将假定实体键值为最终值。如果特定类型的多个实体具有相同键值(例如,0),实体框架将引发异常。由于框架不期望状态为 Added 的实体具有唯一键值,所以从 Added 状态的实体开始可以避免此问题。
  • 从服务方法返回实体时关闭自动延迟加载(EF4 的另一项新增功能)。如果不这样做,序列化程序将触发延迟加载并尝试从数据库中检索其他实体,这样将返回比预期更多的数据(如果您的实体已全部连接,则可能会序列化整个数据库),或者,更有可能的情况是,在序列化程序尝试检索数据之前上下文被释放掉,因此您将收到错误消息。默认情况下,“自我跟踪实体”不会打开延迟加载,但如果创建 DTO 解决方案,则需注意这一点。

总结

.NET 4 版的实体框架大大简化了创建架构合理的 n 层应用程序的过程。对于大多数应用程序,我建议您从“自我跟踪实体”模板开始,该模板可简化过程并实现最大程度的重用。如果服务与客户端之间的更改频率不同,或者如果您需要对传输格式进行绝对控制,则应升级到“数据传输对象”实现。无论您选择哪种模式,请始终牢记反模式和模式所代表的关键原则,永远不要忘记在保存数据之前先对数据进行验证。

posted @ 2011-11-05 18:42  小y  阅读(1095)  评论(0编辑  收藏  举报