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

Entity Framework 4 在 WinForms 开发中的技巧

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (31投票s)

2010 年 6 月 30 日

CPOL

15分钟阅读

viewsIcon

88963

最近我一直在使用 Visual Studio 2010 和 Entity Framework 4 (EF4) 开发一个智能客户端 (WinForms) 应用程序。在这篇博文中,我将尝试分享一些关于 EF4 的局限性和需要注意的地方的技巧。

原文博文 在此

引言 

最近我一直在使用 Visual Studio 2010 和 Entity Framework 4 (EF4) 开发一个智能客户端 (WinForms) 应用程序。在这篇博文中,我将尝试分享一些关于 EF4 的局限性和需要注意的地方的技巧。

为什么选择 Entity Framework (EF)?

在我多年的 .NET 开发生涯中,我使用过 NHibernate、LLBLGen 和 Subsonic 等多种 ORM 框架。每种框架都有其独特的 ORM 方法。我最喜欢的 ORM 是,而且现在仍然是,NHibernate。我喜欢 NH 的开源性质以及活跃的社区。LLBLGen 是我的第二选择,因为它提供了完善的工具,并且实体映射处理起来很方便。Subsonic 非常简洁,使用 Subsonic 进行开发是一种乐趣。但是,这些 ORM 框架有时也会让我抓狂。举几个例子;配置 NHibernate 非常麻烦,尽管他们有一些插件;LLBLGen 有些过于命令式的查询构造;Subsonic 虽然简洁,但在实现一些不太重要的部分(经典“空格”部分)时有时不太合适。哦,我还差点忘了提 LinqToSql,它是微软在 ORM 领域的首次尝试。我也用过 LinqToSql。

自从 EF 首次发布以来,我一直在关注它的进展,并留意人们使用早期 EF 版本时的经验。我的第一印象是:等到下一个主要版本再用。而这个主要版本似乎就是 EF4。我回答“为什么选择 EF”这个问题的原因如下:

  • EF4 作为 .NET Framework 4 的一部分,无需额外安装。
  • EF4 与 Visual Studio 2010 完全集成。
  • EF4 的模型设计器(集成在 VS 2010 中)非常棒。
  • 使用 EF4,您可以轻松地将 POCO 对象持久化到支持的数据库中。
  • 使用 EF4,您可以根据数据库模式生成实体。
  • 复杂类型功能强大。
  • 函数导出(存储过程支持)非常方便。结合复杂类型,这为遗留项目带来了真正的强大功能。

运行时更改 EF 连接字符串

EF4 的连接字符串与普通的 SQL 连接字符串不同。EF4 的连接字符串需要包含关于 EF 模型资源的一些元数据,以便 EF 能够解析模型并应用您通过模型设计器隐式引入的映射等内容。在 Web 应用程序场景中,您可能无需担心运行时更改 EF 连接字符串,因为模型设计器会询问您是否希望将选定的连接信息保存在配置文件中。之后,您只需在部署 Web 应用程序之前更改 SQL 连接信息即可。但在智能客户端场景中,这种方法不太适用,因为您不想在应用程序配置文件中暴露数据库连接信息。最有可能的情况是,您会将数据库连接信息保存在用户特定的设置文件中,并进行一些加密处理,因为您不希望它可供人工阅读,并且在应用程序启动时,您会读取该信息,解密连接字符串并使用该值来建立数据库连接。

这是一个典型的模型设计器生成的 EF 连接字符串:

<add name="eXpressOtoEntities" 
    connectionString="metadata=res://*/eXpressOtoEntities.csdl|res://*/eXpressOtoEntities.ssdl|res://*/eXpressOtoEntities.msl;
    provider=System.Data.SqlClient;
    provider connection string=&quot;Data Source=D00450065;Initial Catalog=eXpressOto;
    Integrated Security=True;MultipleActiveResultSets=True&quot;" 
    providerName="System.Data.EntityClient" />

为了实际使用,您需要构造一个类似上面的连接字符串,可以定义一个工厂方法来构造 EF 连接字符串并为您实例化一个 ObjectContext。下面是一个示例方法:

 public static eXpressOtoEntities CreateObjectContext()
 {
      if (DbConnectionManager.Current == null)
        throw new Exception(
            "Current database connection is not set on DbConnectionManager");

      string conStr = 
         "metadata=res://*;provider=System.Data.SqlClient;provider connection string='" + 
         DbConnectionManager.Current.ConnectionString + "'";
      eXpressOtoEntities ctx = new eXpressOtoEntities(conStr);
      return ctx;
 }

在上面的代码片段中,`eXpressOtoEntities` 是我的 ObjectContext 类的返回类型。`DbConnectionManager` 是一个自定义的静态类,用于加密/存储、加载/解密数据库连接字符串。其余部分都很直接,我们只需进行简单的字符串拼接。

在实体中实现 IDataErrorInfo

IDataErrorInfo 接口是 `System.ComponentModel` 命名空间中的一个标准 .NET 接口,它提供了向用户界面提供可绑定自定义错误信息的功能。大多数 WinForms 控件,无论是标准的还是第三方控件,都通过标准的 .NET ErrorProvider 组件内部支持 IDataErrorInfo。如果您想直接从实体层向最终用户提供关于任何类型验证的错误信息,只需在实体中实现 IDataErrorInfo 即可,这很简单。

EF4 使用 T4 模板根据您的模型生成实体类。当您从数据库模式向模型添加实体时,EF 运行时(实际上是 VS2010)会使用预定义的 T4 模板来生成相应的实体类。据我所知,有两种方法可以为您的 EF 实体类添加额外功能。

  1. 您可以创建自己的 T4 模板,其中包含 IDataErrorInfo 并生成接口的默认实现,然后将此模板提供给 EF 运行时。这是一个稍微复杂的问题,我可以向您保证,您不会想仅仅为了让实体支持 IDataErrorInfo 就进入这个过程。您可以阅读 这篇文章 以获取详细信息。
  2. 标准的 EF T4 模板会为您的实体生成部分类。这意味着您可以通过简单地创建一个部分类文件来为自动生成的实体类添加功能。这种方法很简单,并且在大多数情况下都足够了。下面是一个带有 IDataErrorInfo 实现的示例部分实体类:
 
  partial class ProjectType : IDataErrorInfo
  {   

    #region IDataErrorInfo Members

    public string Error
    {
      get
      {
        StringBuilder sb = new StringBuilder();

        if (!String.IsNullOrWhiteSpace(this["Name"]))
          sb.AppendLine(this["Name"]);

        if (!String.IsNullOrWhiteSpace(this["Prefix"]))
          sb.AppendLine(this["Prefix"]);

        string errors = sb.ToString();
        return !String.IsNullOrEmpty(errors) ? errors : String.Empty;
      }
    }

    public string this[string columnName]
    {
      get
      {
        switch (columnName)
        {
          case "Name":
            return String.IsNullOrWhiteSpace(
                Name) ? "Name can not be empty" : String.Empty;
          case "Prefix":
            return String.IsNullOrWhiteSpace(
                Prefix) ? "Prefixcan not be empty" : String.Empty;
          default:
            return String.Empty;
        }
      }
    }

    #endregion
  }

上面的示例类为我自动生成的 ProjectType 实体类添加了 IDataErrorInfo 实现,用于检查必填字段“Name”和“Prefix”是否具有值。就是这样。

处理通用定义数据

几乎每个系统在运行过程中都需要某种形式的通用定义数据。例如,所有需要注册的网站都会询问国家/地区,这是通用定义数据。大多数情况下,这类数据不需要额外的或复杂的处理,它不是操作的核心,但可能在 BI 中扮演核心角色。系统管理员只需定义数据,应用程序就会将这些数据显示给最终用户。这类数据的模式非常简单,通常只需要一个 Id 和一个 Name 字段,有时也可以包含一个 description 字段。在我们的项目中,我们将这类数据泛化,包含 Id、Name 和 Active 字段,并为每种类型定义了一个表。也许我们可以通过一个数据库表来处理这类数据,并在 EF 侧利用继承策略。但为了简单起见,我们决定放弃这种可能性。

我们决定为管理员提供一个统一的编辑器,以便他们可以操作定义数据。我们设计编辑器使其能够操作接口,以便我们可以处理实现某个契约(某种形式的数据契约)的任何通用数据定义实体。让我们一步一步地进行,并提供一些代码。

步骤 1:定义数据契约,即 IDefinitionDataEntity 接口

public interface IDefinitionDataEntity
{
    Int32 Id { get; set; }
    string Name { get; set; }
    bool Active { get; set; }
}

这很简单,我们只需定义一个数据契约来定义我们通用数据的结构。

步骤 2:用 IDefinitionDataEntity 接口标记定义数据实体

  [Description("Vehicle Kinds")]
  partial class VehicleKind: IDefinitionDataEntity
  {   
  }

我们只需要为 VehicleKind 实体类(由 EF 自动生成)定义一个部分类。由于我们的数据库模式包含 Id、Name 和 Active 列,实际上 EF 生成的 VehicleKind 类已经包含这些属性,我们只需用 IDefinitionDataEntity 标记我们的部分类。

注意:Description 属性是 .NET 框架中的一个标准属性,位于 `System.ComponentModel` 命名空间。我们将使用此属性在数据编辑器中渲染用户友好的实体信息。

步骤 3:使用反射获取所有实现 IDefinitionDataEntity 接口的实体类型

public class DefinitionDataEntityTypeInfo
{
  public Type EntityType { get; set; }
  public string Description { get; set; }
  public static ReadOnlyCollection<DefinitionDataEntityTypeInfo> TypeInfos 
  { 
     get;
     private set; 
  }
  static DefinitionDataEntityTypeInfo()
  {
    PrepareTypeInfos();
  }
}

DefinitionDataEntityTypeInfo 类只是一个辅助类,用于保存有关定义数据实体类的信息,这些信息通过反射填充。`TypeInfos` 是我们存储所有定义实体数据的地方。

private static void PrepareTypeInfos()
{ 
      Type tt = typeof(IDefinitionDataEntity);
      
      var results = ( 
                      from a in 
                      ( from type in Assembly.GetExecutingAssembly().GetTypes()
                        where type.GetInterface(tt.FullName, true) != null
                        select type 
                      )
                      where a.IsClass == true
                      select new DefinitionDataEntityTypeInfo { EntityType = a}
                    ).ToList<DefinitionDataEntityTypeInfo>();

      results.ForEach
        (
          delegate(DefinitionDataEntityTypeInfo t)
          {
            var z = (from x in t.EntityType.GetCustomAttributes(typeof(
                       DescriptionAttribute),false)
                     select (DescriptionAttribute)x).FirstOrDefault < DescriptionAttribute>();
            if (z != null)
              t.Description = z.Description;
          } 
        );

      TypeInfos = results.AsReadOnly();
  }

DefinitionDataEntityTypeInfo 类的 `PrepareTypeInfos` 静态方法在静态构造函数中被调用,它会检查所有实现 IDefinitionDataEntity 接口的实体类,并将检查数据存储在 `TypeInfos` 静态属性中。

步骤 4:获取定义实体的实体集名称

为了查询和修改数据,我们必须为我们的定义实体类提供强类型的实体集,或者在我们的对象上下文中检查每个实体类型的实体集名称。为了满足此要求,我们为 ObjectContext 实现创建了一个部分类,并为我们的目的引入了两个方法:`GetEntitySet` 和 `GetEntitySetName`。这是实现:

partial class eXpressOtoEntities
{
  // .....
  // Some other stuff here
  // .....

  public EntitySetBase GetEntitySet(Type entityType)
  {    
      EntityContainer container = 
          this.MetadataWorkspace.GetEntityContainer(this.DefaultContainerName, 
          DataSpace.CSpace);
      EntitySetBase entitySet = container.BaseEntitySets.Where(
          item => item.ElementType.Name.Equals(entityType.Name))
                                                        .FirstOrDefault();

      return entitySet;
  }

  public string GetEntitySetName(Type entityType)
  {
      EntitySetBase esb = GetEntitySet(entityType);
      return esb != null ? esb.Name : String.Empty;
  }
}

我们使用附加到 ObjectContext 实现(eXpressOtoEntities)的元数据(实际上是包含在自动生成的 EDM 中)来检查特定定义实体类型的实体集名称。

注意:此处介绍的方法也可以用于获取任何 EF 实体类的实体集或实体集名称。

步骤 5:加载定义数据

最后一步是根据用户的选择,在我们的统一编辑器中加载我们强类型的定义数据。这是代码。

private void LoadData(Type entityType)
{
    ObjectQuery<IDefinitionDataEntity> qry =
        _ctx.CreateQuery<IDefinitionDataEntity>(String.Format("{0}", _currentSetName));

    var z = (from x in qry
                where x.Active
                orderby x.Name
                select x);
    _currentSet = z;
     bs.DataSource = _currentSet;
}

注释

  • _currentSet 的类型为 object,用于保存我们的对象查询返回的数据,我们将其用作 bindingSource (BindingSource) 的 DataSource。
  • 我们只加载活动的实体(记录)。
  • entityType 参数是用户通过查找控件选择定义数据的结果。我们在步骤 3 中确定了定义实体的真实类型,并填充了查找控件。

最终编辑器外观如下:

这是一个土耳其语应用程序,所以 Tanımlar 是用户选择定义实体的查找框,查找框中显示的描述直接来自我们在步骤 2 中提到的 Description 属性。

处理并发异常

EF4 通过实体上的 Concurrency Mode 属性支持乐观并发场景。您只需为要包含在并发检查过程中的任何属性设置 Concurrency Mode = Fixed,EF 就会为您处理其余的事情。在后台,EF 会在 Update 和 Delete 语句的 where 子句中包含参与并发检查的属性的旧值(即实体首次在 ObjectContext 中具体化时的值)。当语句针对数据库执行,并且返回的行数为零时,EF 会推断该记录已被另一个用户/进程修改,这反过来会导致引发并发异常。对于大多数场景来说,这既简单又强大。但我应该警告您有关使用时间戳列进行此场景。通常,SQL Server 中的时间戳列用于标识自上次获取记录以来是否已修改。但在父/子结构中,在子实体更新的情况下,EF 也会对父实体发出一个伪更新,这会导致父实体的时间戳值更新,即使实体未被另一个用户或进程显式修改,也会导致父实体的并发异常。这种行为是由于假设当父实体的子实体被修改时,也可能导致父实体的概念性修改。但这种假设并不总是适用,可能大多数时候都不适用。在这种情况下,您需要实现自己的修改标记策略。您可以在 这里 找到一个简单的解决方案。我们以其中的示例为起点实现了自己的解决方案。

对乐观并发的介绍太多了,总之。当您遇到并发异常时,有两种选择:

  1. 告知用户问题并自动重新加载数据。
  2. 告知用户问题,并允许用户决定如何处理;重新加载数据(StoreWins)或覆盖数据库中的数据(ClientWins)。

我们选择了选项 2,并显示一个对话框,让我们的用户选择下一步做什么。这种实现并不容易,特别是如果您正在处理一个复杂的用户界面,该界面为单个实体(记录)呈现大量相关信息。我们在 ObjectContext 的 Refresh 方法时遇到了一些问题。您需要确切地知道要刷新哪个实体或实体集,有时这是不可能的,因为您只是简单地将用户界面绑定到实体的属性。

这是一个示例 SaveChanges 实现,我们在其中尝试处理并发异常:

    /// <summary>
    /// Asks user for confirmation if there are changes on the object context 
    /// and persist changes
    /// </summary>
    /// <param name="confirm">Ask user for confirmation</param>
    /// <returns>Returns true if user does not allow changes to be persisted</returns>
    private bool SaveChanges(bool confirm)
    {
      if (!SessionConsts.CanPrincipalEditProjects)
        return true;

      ucProjectEditor1.EndEdit();
      ucComissionMemberList1.EndEdit();

      bool isNewProject = ucProjectEditor1.IsNew;
      // 1)Object contex has modified, deleted or new objects 
      // 2)or we try to save a new object
      if (_objectContext.HasChanges || isNewProject)
      {
        if (!confirm || MessageBoxHelper.ShowYesNo("Save changes?") == 
          System.Windows.Forms.DialogResult.Yes)
        {

          // Validate project properties
          bool projectOk = ucProjectEditor1.ValidateUserInput();
          
          //Validate comission member properties
          bool membersOk = ucComissionMemberList1.ValidateUserInput();

          // We have problems with user input
          if (!projectOk || !membersOk)
          {
            // Build the error message
            StringBuilder errors = new StringBuilder();
            if (!projectOk)
            {
              errors.AppendLine("Project definition errors.");
              errors.AppendLine(ucProjectEditor1.GetUserInputErrors());
            }
            if (!membersOk)
            {
              errors.AppendLine(ucComissionMemberList1.GetUserInputErrors());
            }
            MessageBoxHelper.ShowError(
            "Can not save project.Please correct the errors.\r\n\r\n" + errors.ToString());
            // Cancel save operation
            return false;
          }

          // We have a new project so we have to add that object to context
          if (isNewProject)
            _objectContext.Projects.AddObject(ucProjectEditor1.Project);

          using (WaitDialogForm waitDlg = GeneralUtils.WaitDlg("Saving changes..."))
          {
            try
            {
              // Try to save
              _objectContext.SaveChanges(SaveOptions.AcceptAllChangesAfterSave | 
                  SaveOptions.DetectChangesBeforeSave);
              
              // Save is sucessfull so we refresh editor captions
              ucProjectEditor1.RefreshCaption();
              ucComissionMemberList1.RefreshCaption();
              
              // If that was a new project we refresh the project list 
              // without reloading the current project in the editors
              if (isNewProject)
                LoadProjects(false);
            }
            // We got optimistic concurrency error
            catch (OptimisticConcurrencyException oce)
            {
              // Ask user what shall be done
              // Option 1) Load data from database
              // Option 2) Keep user data and automatically save
              waitDlg.Hide();
              RefreshMode mode = OptimisticConcurrencyExceptionDlg.ShowForm(oce);
              try
              {
                // Refresh the project and the comission members collection
                _objectContext.Refresh(mode, ucProjectEditor1.Project);
                _objectContext.Refresh(mode, ucProjectEditor1.Project.ComissionMembers);

                // Project was delete in another session but we try to update in
                // current session
                // In this case refresh suceeeds but the object is not in the context
                // anymore so we need to reload another project to the editor
                if (!IsProjectStillInContext())
                {
                  waitDlg.Hide();
                  MessageBoxHelper.ShowError(
                      "Proje başka bir kullanıcı tarafından silinmiş veya değiştirilmiş.");
                  LoadProjects(true);
                  return false;
                }
              }
              // If ClientWins is selected by the user and master or details
              // records can not be found we get an exception
              catch (Exception)
              {
                waitDlg.Hide();



                MessageBoxHelper.ShowError(
                    "Proje veya bağlı kayıtlar başka bir kullanıcı tarafından" +
                    "değiştirilmiş veya silinmiş.\r\nLütfen proje listesini" +
                    "tazeleyerek tekrar deneyiniz.");
                return false;
              }

              if (mode == RefreshMode.ClientWins)
              {
                try
                {
                  waitDlg.Show();
                  _objectContext.SaveChanges(SaveOptions.AcceptAllChangesAfterSave | 
                      SaveOptions.DetectChangesBeforeSave);
                  ucProjectEditor1.RefreshCaption();
                  ucComissionMemberList1.RefreshCaption();
                }
                // We can not apply client values to the master or detail records.
                catch (Exception)
                {
                  waitDlg.Hide();
                  MessageBoxHelper.ShowError(
                      "Can not save changes.\r\nProject record or any other" +
                      "related record was deleted by another session\r\nPlease" +
                      "refresh the Project list and try again.");
                  LoadProjects(true);
                  return false;
                }

              }
            }
            catch (UpdateException upex)
            {
              waitDlg.Hide();
              if (!GeneralUtils.TryDisplayUniqueIndexError(upex))
                throw upex;
              else
                return false;
            }
            return true;
          }
        }
        //else
        //{
        //  if (!isNewProject && !isExplicitSave)
        //    _objectContext.Refresh(RefreshMode.StoreWins, ucProjectEditor1.Project);
          
        //  return true;
        //}
      }


      return true;
    }

这个方法有很多代码,但我只想让您专注于这些行:

  • 第 56 行:我们调用 SaveChanges 将更改持久化到数据库。
  • 第 68 行:我们捕获乐观并发异常。
  • 第 74 行:我们显示选项对话框,让我们的用户选择下一步做什么。
  • 第 78 行和第 79 行:我们调用 Refresh,使用用户为我们的 Project 实体和 ComissionMembers EntitySet 选择的模式(幸运的是,我们知道要刷新什么)。
  • 第 100-118 行:如果用户选择了 ClientWins 模式,我们会重新发出 SaveChanges,以便用户的更改将被持久化到数据库并覆盖数据库中的值。

这是我们的对话框,在发生并发异常时,我们询问用户下一步做什么。同样,对话框是土耳其语的,为了澄清,第一个选项表示 StoreWins,即用户的更改将被丢弃,第二个选项表示 ClientWins,即用户所做的更改将覆盖数据库。

WinForms 中的 UnitOfWork (ObjectContext) 决策

UnitOfWork 是所有 ORM 框架中隐含或显式引入的核心概念,并且每种 ORM 都通过某些构造来包装/实现这个概念。EF4 拥有 ObjectContext 类,EF 会为您生成一个继承自 ObjectContext 的命名类。如果您希望 EF 从数据库获取数据或持久化数据,您的入口点就是您命名的 ObjectContext 类。EF 会在您的 ObjectContext 实例中具体化您的实体,一旦您将实体从对象上下文中分离,您就无法再将它们持久化到数据库中了。

创建 ObjectContext 实例不是性能敏感的操作,但当您开始用实体填充对象上下文时,您必须非常小心。如果您将对象上下文实例长时间保留在内存中并执行过多的数据操作,您的对象图可能会变得过于复杂,并且您的对象上下文中将包含数千个对象,这反过来会导致客户端出现严重的性能问题。因此,在实例化对象上下文实例时要小心,并仔细规划它们的生命周期,并在完成后不要忘记释放它们。

在 Web 应用程序场景中,大多数 ORM 框架建议 UnitOfWork 的边界应由请求定义,即请求到达时 UnitOfWork 开始,请求完成/处理时 UnitOfWork 结束。这对于 Web 来说是一种非常有效的策略,但对于智能客户端应用程序,我们没有请求。我们必须仔细规划,并可能根据应用程序流程应用不同的策略。无论如何,对于 WinForms,我们仍然有一些线索:

  • 每个窗体的 ObjectContext
  • 每个用户控件的 ObjectContext
  • 每个用例的 ObjectContext

在我们的应用程序中,我们使用了以上所有策略。我们有带有单个对象上下文的窗体,我们有管理自己对象上下文的用户控件,也有一些用户控件仅使用父窗体的对象上下文。在某些向导式交互场景中,我们也为每个用例使用单独的对象上下文。总而言之,在定义您的 UnitOfWork 时要小心,否则您将遇到内存问题,并且必须手动处理分离的对象。

使用 Object Context 进行审计日志记录

大多数应用程序都提供某种形式的审计/跟踪日志,具有不同的详细程度。有些应用程序只记录谁/何时创建/修改了记录作为实际记录的属性,而有些应用程序则在单独的数据库或表中保存更详细的信息。在我们的应用程序中,我们同时使用了这两种方法,对于一些不太关键的数据,我们只跟踪谁/何时创建/修改了记录,对于一些敏感且更复杂的数据,我们在单独的表中跟踪更改。

使用 EF4 进行审计日志记录非常简单,您只需挂钩到 ObjectContext 类的 SaveChanges 事件,并从对象上下文实例的 ObjectStateManager 获取有关已更改实体的信息。这很简单,这里有一个例子:

public class ContextInterceptor : IDisposable
{
    private eXpressOtoEntities context;

    public ContextInterceptor(eXpressOtoEntities context)
    {
      this.context = context;
      this.context.SavingChanges += new EventHandler(WhenSavingChanges);
    }

 
    public void Dispose()
    {
      if (this.context != null)
      {
        this.context.SavingChanges -= new EventHandler(WhenSavingChanges);
        this.context = null;
      }
    }
    
    void WhenSavingChanges(object sender, EventArgs e)
    {
      // Query ObjectStateManager and get Modified, Added and Deleted entities
      foreach (var item in this.context.ObjectStateManager.GetObjectStateEntries(
          System.Data.EntityState.Modified | System.Data.EntityState.Added | 
          EntityState.Deleted))
      {
        object entity = item.Entity;
        // Perform audit logging or whatever you want
       }
     }
}

ContextInterceptor 类只是一个实用类,您也可以将代码写在您的 ObjectContext 类中。

 partial class eXpressOtoEntities
 {
  
    partial void OnContextCreated()
    {     
      // Automatically attach an ContextInterceptor on each new context:
      new ContextInterceptor(this);
    }
 }

每当创建新的 ObjectContext 实例时,我们都会简单地创建一个 ContextInterceptor

到目前为止一切顺利,挂钩很简单,插入日志记录也很简单;创建审计日志实体并将它们附加到上下文并像往常一样保存。但是有一点您应该注意,那就是乐观并发。在调用 SaveChanges 并执行您的 WhenSavingChanges 委托之后,您将收到乐观并发异常,在将日志实体附加到上下文之后,您将收到异常。如果您为用户提供了恢复方法(如我在“处理并发异常”部分所述),用户将能够对同一个上下文发出另一个 SaveChanges 调用,这将导致创建另一组审计日志实体,并且您会将重复的日志条目持久化到数据库。为了避免这个问题,您应该在 WhenSavingChanges 委托的开头识别并删除分离的日志条目。这是 ContextInterceptor 类中 WhenSavingChanges 方法的修改版本:

 void WhenSavingChanges(object sender, EventArgs e)
 {
      context.DetachAdded<VehicleModelZeroPriceLog>();
      context.DetachAdded<JobOrderStateLog>();
      
      // Query ObjectStateManager and get Modified, Added and Deleted entities
      foreach (var item in this.context.ObjectStateManager.GetObjectStateEntries(
          System.Data.EntityState.Modified | System.Data.EntityState.Added | 
          EntityState.Deleted))
      {
        object entity = item.Entity;
        // Perform audit logging or whatever you want
      }
  }

// Excerpt of eXpressOtoEntities demonstrating DetachAdded<T> method implementation
partial class eXpressOtoEntities
{
  // .....
  // Other stuff here
  // .....
 public void DetachAdded<T>()
 {     
   List<ObjectStateEntry> logEntries = 
   ObjectStateManager.GetObjectStateEntries(
        EntityState.Added).Where<ObjectStateEntry>(
        e => e.Entity.GetType() == typeof(T)).ToList<ObjectStateEntry>();
   logEntries.ForEach(delegate(ObjectStateEntry e)
   {
     this.Detach(e.Entity);
   });
 }
}

DetachAdded<T> 方法会简单地分离特定类型的已添加实体,在本例中,我们使用此方法来分离任何新创建的 VehicleModelZeroPriceLogJobOrderStateLog 类型的实体。因此,通过这个小程序,我们将在并发异常恢复场景中避免重复的日志条目。

结论

使用 EF4 的体验很顺利,有了 ORM 的背景,发现本文中提到的不足之处很容易。我甚至在与 EF4 合作的过程中感到愉快。我还向微软提交了一些关于 Visual Studio 2010 和模型设计器的 bug 和改进建议。

我还期待测试 EF4 在更分层架构中的适用性,以及是否可以实现 Repository Pattern。我将尝试在体验 EF4 的不同方面时发布我的经验。

敬请关注...

© . All rights reserved.