使用Entity Framework Self-Tracking Entity保存临时数据
在本文中,我将向您展示如何使用 Entity Framework 自跟踪实体在企业应用程序中保存临时数据。
引言
在企业应用程序中,开发人员经常会遇到一个情况,那就是如何设计一个临时数据保存功能。临时数据保存功能可以减少因会话超时、计算机宕机或网络问题导致的数据丢失的几率。此外,临时数据保存功能还可以使某些业务流程持续数个工作日,而不会锁定现有数据或减慢其他业务流程的速度。设计临时数据保存的方法有很多,例如将数据保存到带有状态标志的业务表中,或者将数据保存到业务表的镜像表中。在本文中,我将向您展示如何使用 Entity Framework 自跟踪实体(缩写为 STE)来处理这种情况,它不仅可以保持数据模型(数据库表)的整洁,还可以提供足够的扩展性来处理更多的临时数据保存请求。
临时数据保存
在普通的企业应用程序中,当需要一次性将大量数据输入系统,或者输入的数据在其他用户可见之前必须经过验证和批准时,用户可能会遇到数据丢失或草稿数据问题。试想一下,如果用户在基于 Web 的应用程序中输入了数小时的数据,然后点击“提交”按钮尝试保存数据,他们有时会收到“会话超时”的提示,而不是“保存成功”。用户会多么沮丧?另外,试想一下,当业务分析师在新客户数据输入系统时,在客户数据获得业务部门经理的验证和批准之前,其他部门的用户就已经可以看到并使用该客户进行业务操作了。这可能会造成多么严重的后果,又需要清理多么大的烂摊子?为了解决上述问题,我们可以为用户提供一种临时保存数据的方法。临时保存的数据仅由数据输入者或特定人员可见,这使得数据输入者能够定期保存工作,而不必等到最后。这还可以防止其他部门用户看到和使用草稿数据。
Entity Framework 自跟踪实体
在应用领域驱动设计之前,所有业务数据都在企业应用程序中使用 RecordSet 进行处理,因此,临时数据保存最常见的解决方案是在业务表中设置一个状态标志来指示业务数据尚未完成,或者使用业务表的镜像表来存储临时数据。这些解决方案的缺点是它们非常繁琐,并且需要对现有应用程序进行大量更改。此外,这些解决方案会导致大量额外数据或表,弄乱整个应用程序。应用领域驱动设计后,我们设计应用程序以拥有一个集中的域层,该层使用域实体来存储业务数据。我们可以序列化域实体并将其保存到临时位置。如何支持域实体的序列化和反序列化?
我们可以将域实体设计为 Entity Framework 自跟踪实体。Entity Framework 是 Microsoft 提供的一个 ORM 框架。它用于处理域模型和数据模型之间的范式不匹配。通过使用它,我们将拥有一个优雅的域模型,而无需担心如何将其持久化到数据库。设计域模型有很多种方法。例如,POCO 风格的域模型。POCO 风格的域模型是一个简单的 .NET 类,可以直接由 ORM 框架处理,而无需任何 ORM 所需的属性或方法。它的简洁和易用性吸引了许多企业应用程序开发人员在他们的应用程序中使用它。然而,它需要更多的工作来支持更改跟踪和序列化,尤其是 XML 序列化。为了使一个类支持更改跟踪和序列化,Entity Framework 提出了自跟踪实体(Self-Tracking Entity)的概念。严格来说,自跟踪实体不是纯粹的 POCO 风格的域实体,因为在域实体中添加了许多属性和方法来支持自跟踪和序列化。但是,由于所有自跟踪相关的属性和方法以及序列化/反序列化支持都是在源代码级别添加的,因此它不依赖于 Entity Framework,所以您仍然可以使用域模型与其他 ORM 框架,例如 NHibernate。自跟踪实体的设计目的是允许域实体在服务应用程序中序列化为 XML,并通过 Web 服务发送,然后在 Web 服务客户端应用程序中反序列化回来。在这里,我并不是真正这样使用它。我将利用自跟踪实体的序列化能力来帮助进行临时数据保存。
想法
这个想法是使用自跟踪实体作为域实体,并有一个表来存储域实体对象的序列化 XML。通过这样做,我们获得了以下好处:
- 临时数据与最终数据完全分开。
- 易于实现。只需添加一个表,无需更改任何现有业务表或代码。
- 可扩展。为其他域实体添加临时数据保存支持不会导致对任何现有表或代码的更改。
实现
这里使用一个简单的 ASP.NET MVC 应用程序来演示如何实现这个想法。演示应用程序设计为一个员工信息管理应用程序。您可以添加、更新或删除员工。
Data Model
演示应用程序中只有两个表,一个业务表 Employee 和一个临时数据存储表 TempData。

TempData 表中的 Data 字段用于存储序列化的业务对象 XML。显然,DataType 是 XML。
Visual Studio 解决方案结构
演示应用程序中有四个项目:Demo.Base、Demo.Data、Demo.Models 和 Demo.Web。
- Demo.Base:演示应用程序的公共库。它包含一个 DataHelper 来处理序列化和反序列化,以及一个用于依赖注入的接口。
- Demo.Data:数据访问层。它实现了 Repository 模式来封装 Entity Framework。
- Demo.Models:域层。它包含自跟踪实体风格的域实体和数据访问接口。
- Demo.Web: Web 用户界面层。它基于 ASP.NET MVC 构建,具有良好的关注点分离和面向测试的结构。
实体类和上下文类
支持临时数据保存的最重要类位于 Demo.Data 项目和 Demo.Models 项目中。让我们看看如何生成它们。首先,我们需要从数据模型(数据库)中通过 Entity Framework Entity Data Model 向导添加 EntityDataModel.edmx。

通过在 Entity Data Model 的上下文菜单中选择“添加代码生成项…”,您可以将 ADO.NET 自跟踪实体生成器模板添加到项目中。

命名为 STE.tt。

点击“添加”按钮后,您会看到项目中有两个文件被添加:STE.tt 和 STE.Context.tt。STE.tt 是您的域实体类的 T4 模板文件,STE.Context.tt 是 Entity Framework 上下文类的 T4 模板文件。由于 Demo.Data 项目是数据访问层,我们需要将 STE.tt 文件移动到 Demo.Models 项目中。移动后,需要在 STE.tt 文件中进行一个小小的更改,以便将输入文件 EntityDataModel.edmx 指向原始位置。

同时,需要更新 STE.Context.tt 以获得正确的 Custom Tool Namespace,这样上下文类仍然可以引用域实体类。

如果您查看生成的 Employee 类,您会发现除了来自 Employee 表的属性之外,还有一些额外的属性和方法。由自跟踪实体模板添加的额外属性和方法用于跟踪更改以及用于序列化/反序列化。

此外,该类在类上有一个 DataContract 特性,在属性上有一个 DataMember 特性,以支持序列化和反序列化。

保存临时数据
Demo.Web 项目将自跟踪实体类和上下文类结合起来实现临时数据保存。从 Employee 控制器中的 CreatTempData 操作方法可以看出,我们如何将新创建的 Employee 对象保存为临时数据。
- 使用用户输入的数据创建一个 Employee对象。
- 将新的 Employee对象序列化为 XML 字符串。
- 清除上下文。
- 创建一个 TempData对象,并将新Employee对象 XML 存储到Data属性中。
- 调用存储库的 Create方法将新的TempData对象附加到上下文中。
- 将新创建的 TempData对象提交到数据库。
注意:由于我们在处理数据库操作时使用了 Unit-of-Work 模式,因此需要调用 Context.Clear() 来关闭当前的 Unit-of-Work,以便后续的 Context.Commit 将在一个新的 Unit-of-Work 中进行。这将防止除 TempData 之外的其他对象被提交到数据库。
这里显示了 CreateTempData 操作方法的代码片段。
[HttpPost]
[ActionName("Create")]
[AcceptVerbs(HttpVerbs.Post)]
[AcceptParameter(Name="button", Value="Create Temp Data")]
public ActionResult CreateTempData(EmployeeVM employeeVM)
{
    IContext context = Demo.Models.Registry.Context;
    ITempDataRepository tempDataRepository = 
      Demo.Models.Registry.RepositoryFactory.GetTempDataRepository();
    if (ModelState.IsValid)
    {
        // New an Employee oject with user entered data
        Employee employee = new Employee();
        employee.Name = employeeVM.Name;
        employee.Title = employeeVM.Title;
        employee.Address = employeeVM.Address;
        // Serialize the new Employee object to XML string
        string xml = DataHelper.SerializeEntity<Employee>(employee);
        // Clear context
        context.Clear();
        // New a TempData object and store XML of the new Employee object into Data property
        TempData tempData = new TempData();
        tempData.Data = xml;
        tempData.LastUpdated = DateTime.Now;
        // Call Create method of repository to attach the new TempData object with context
        tempDataRepository.Create(tempData);
        // Commit the new created TempData object into database
        context.Commit();
        return RedirectToAction("Index", "Home");
    }
从 Employee 控制器的 SaveEmployee 操作方法可以看出,我们如何将临时数据保存到实际的业务表中。
- 从数据库中查找 TempData对象。
- 将 Data属性的值反序列化回Employee对象。
- 将 Employee对象附加到上下文中。
- 使用用户输入的最新数据更新 Employee对象。
- 从上下文中删除 TempData对象。
- 将 Employee对象提交到数据库,并从数据库中删除TempData对象。
这里列出了 SaveEmployee 操作方法的代码片段。
[HttpPost]
[ActionName("UpdateTempData")]
[AcceptVerbs(HttpVerbs.Post)]
[AcceptParameter(Name = "button", Value = "Save Employee")]
public ActionResult SaveEmployee(EmployeeVM employeeVM)
{
    IContext context = Demo.Models.Registry.Context;
    ITempDataRepository tempDataRepository = 
      Demo.Models.Registry.RepositoryFactory.GetTempDataRepository();
    IEmployeeRepository employeeRepository = 
      Demo.Models.Registry.RepositoryFactory.GetEmployeeRepository();
    if (ModelState.IsValid)
    {
        // Find the TempData object from database
        TempData tempData = 
          tempDataRepository.Find(td => td.TempDataID == employeeVM.LinkTempDataID);
        // Deserialize the value of data property back into Employee object
        Employee employee = DataHelper.DeserializeEntity<Employee>(tempData.Data);
        
        // Attach the Employee object into context
        employeeRepository.Attach(employee);
        
        // Update the Employee object with the latest data entered by user
        employee.Name = employeeVM.Name;
        employee.Title = employeeVM.Title;
        employee.Address = employeeVM.Address;
        // Delete TempData object from context
        tempDataRepository.Delete(tempData);
        // Commit the Employee object into database
        // and delete TempData object from database
        context.Commit();
        return RedirectToAction("Index", "Home");
    }
    return View(employeeVM);
}
以下是存储在 TempData 表中的示例 Employee 对象 XML。
<Employee xmlns="http://schemas.datacontract.org/2004/07/Demo.Models.Domain" 
         xmlns:i="http://www.w3.org/2001/XMLSchema-instance" 
         xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/" z:Id="i1">
  <Address>Main Street, Edison, NJ</Address>
  <ChangeTracker z:Id="i2">
    <ExtendedProperties />
    <ObjectsAddedToCollectionProperties />
    <ObjectsRemovedFromCollectionProperties />
    <OriginalValues />
    <State>Added</State>
  </ChangeTracker>
  <EmployeeID>0</EmployeeID>
  <Name>Tom</Name>
  <Title>Manager</Title>
</Employee>
摘要
借助 Entity Framework 自跟踪实体,我们可以轻松实现临时数据保存功能。这个解决方案简单、优雅且可扩展。此外,它对现有应用程序的影响非常小。如果您确实不想使用 Entity Framework 自跟踪实体,您可以采用 POCO 风格的域实体,并使用 DTO 作为媒介来将域实体对象序列化和反序列化到 XML。
Using the Code
该代码在 Visual Studio 2010 中开发。您需要首先在 SQL Server 中创建一个名为 SaveTempDataWithSTE 的数据库,然后运行附带的 SaveTempDataWithSTE.sql 脚本来创建其中的 Employee 表和 TempData。运行代码之前,请记住更新配置文件中的连接字符串为您本地的连接字符串。




