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

代码分析 Entity Framework 5

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (9投票s)

2012年11月27日

CPOL

4分钟阅读

viewsIcon

59082

downloadIcon

721

挂钩 SaveChanges,并查看 T-SQL 向数据库发出的确切内容,而无需附加分析器。

引言

本文将介绍如何连接到 EntityFramework 的 OnSaveChanges 事件(该事件深埋在框架中)。它还将解释如何在调用 SaveChanges 时提取实际提交的 T-SQL 语句。

很多时候,我都会将分析器附加到我的数据库中,以查看 EF 实际提交的内容。代码中缺乏此功能让我感到失望,所以我决定编写一个 EntityFramework 5 的扩展来实现这一点。

此扩展在DbContext上公开了一个名为HookSaveChanges()的方法。这允许您提供一个回调函数,以便每次在您的DbContext上调用保存更改时都会调用该函数。

使用代码

在详细介绍扩展的工作原理之前,我将提供一个简单的使用示例。如果您不感兴趣它的工作原理,只需研究下面的代码,下载扩展程序并将其插入即可。

首先,您需要一个数据库上下文和一个表,这是一个我将使用的非常简单的示例

public class TestContext : DbContext 
{
    public TestContext()
        : base(Properties.Settings.Default.TestContext)
    {
        Database.SetInitializer<TestContext>(null);
    }

    public DbSet<TestTable> TestTables { get; set; }

}

[Table("Test")]
public class TestTable
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Col1 { get; set; }

    [MaxLength(50)]
    public string Col2 { get; set; }
}

以下是挂钩保存更改事件所需的代码。

class Program
{
    private static void Main(string[] args)
    {
        //create a new instance of your context
        var context = new TestContext();
        //call HookSaveChanges extension method
        context.HookSaveChanges(FuncDelegate);
        //do some db changes
        var tt = new TestTable() { Col2 = "Testing 123" };
        context.TestTables.Add(tt);
        //call save changes
        context.SaveChanges();
    }
    private static void FuncDelegate(DbContext dbContext, string command)
    {
        Console.WriteLine(command);
        Console.ReadLine();
    }
}

如果您在控制台应用程序中运行上述代码,则应该看到以下输出

详细信息

描述挂钩工作原理的最佳方法是按照我构建代码的顺序引导您完成代码。我首先要做的是确定是否存在这样的事件。通过查看反射代码(无需下载源代码),我发现ObjectContext包含一个名为SavingChanges的公共事件。如果在SaveChanges方法的开头调用此事件。不幸的是,ObjectContext在从DbContext的调用链中向下嵌套了几层。为了访问此实例并向其附加委托,我们需要通过反射来遍历调用链。

首先是InternalContext,它在DbContext中声明为:

internal virtual InternalContext InternalContext {get;}

因此,通过一些类型反射魔法,我们可以忽略它是内部的事实并获取其值

var internalContext = _context.GetType()
                               .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance)
                               .Where(p => p.Name == "InternalContext")
                               .Select(p => p.GetValue(_context,null))
                               .SingleOrDefault();

好的,InternalContext解决了,接下来是ObjectContext(我们感兴趣的那个)。与上一个相同,区别在于此属性被标记为公共属性

var objectContext = internalContext.GetType()
                           .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                           .Where(p => p.Name == "ObjectContext")
                           .Select(p => p.GetValue(internalContext,null))
                           .SingleOrDefault();

现在我们有了ObjectContext实例,我们可以获取我们感兴趣的事件SavingChanges。与上面的代码类似,它是这样提取的

var saveChangesEvent = objectContext.GetType()
                                .GetEvents(BindingFlags.Public | BindingFlags.Instance)
                                .SingleOrDefault(e => e.Name == "SavingChanges");

有了EventInfo引用和ObjectContext引用,我们现在可以创建一个委托并将处理程序添加到事件中

var handler = Delegate.CreateDelegate(saveChangesEvent.EventHandlerType, this, "OnSaveChanges");
saveChangesEvent.AddEventHandler(objectContext,handler);

如果您只对获取SaveChanges事件感兴趣,那么您就完成了。每次在您的上下文中调用SaveChanges时,您的事件处理程序都会触发!如果您有兴趣了解如何在数据库上提交SaveChanges时提取提交的T-SQL语句,请继续阅读。

注意:本节大量使用了 Entity Framework 未公开的内部结构,这些结构可能会在 EF 的新版本中发生更改。此代码已使用 .NET4.0 和 EF5 (4.4) 进行了测试

在我深入研究代码之前,最好先概述一下调用SaveChanges时发生的情况。这是一个显示典型流程的序列图(注意,此图中缺少大量信息)。它只显示我们感兴趣的组件用法

这里的目标是生成一个DbCommand对象集合(如果您想要更多信息,请查看DynamicUpdateCommand的源代码以了解示例),这些对象将在循环中依次执行。因此,我们可以从图中看出,就命令生成而言,ObjectContextEntityAdapter并没有真正做太多工作。大部分工作发生在UpdateTranslatorUpdateCommand(抽象类)的实现本身中。

为了从每个命令中检索CommandText,我们需要复制此功能并访问每个UpdateCommand(通常是DynamicUpdateCommand,正如您调试代码时所看到的)的基础DbCommand。最好的方法是在我们之前挂钩的SaveChanges的回调函数中执行此操作,因为我们可以使用“object sender”参数直接获取ObjectContext的新引用。

第一个目标是创建一个UpdateTranslator实例。此类的构造函数有四个参数:ObjectStateManagerMetadataWorkspaceEntityConnection和Int? ConnectionTimeout。我们可以使用与之前类似的反射来获取我们的参数

var conn = sender.GetType()
                 .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                 .Where(p => p.Name == "Connection")
                 .Select(p => p.GetValue(sender,null))
                 .SingleOrDefault();
var entityConn = (EntityConnection) conn;
  
var objStateManager = (ObjectStateManager)sender.GetType()
      .GetProperty("ObjectStateManager", BindingFlags.Instance | BindingFlags.Public)
      .GetValue(sender,null);
var workspace = entityConn.GetMetadataWorkspace();

然后随后创建我们的UpdateTranslator实例

var translatorT = 
    sender.GetType().Assembly.GetType("System.Data.Mapping.Update.Internal.UpdateTranslator");
var translator = Activator.CreateInstance(translatorT,BindingFlags.Instance | 
    BindingFlags.NonPublic,null,new object[] {objStateManager,workspace,
    entityConn,entityConn.ConnectionTimeout },CultureInfo.InvariantCulture);

好的,接下来我们需要在UpdateTranslator上调用ProduceCommands。这将返回一个UpdateCommands集合,我们可以对其进行枚举

var produceCommands = translator.GetType().GetMethod(
   "ProduceCommands", BindingFlags.NonPublic | BindingFlags.Instance);
var commands = (IEnumerable<object>) produceCommands.Invoke(translator, null);

最后,我们可以枚举返回结果并提取DbCommand。您可能知道如何处理DbCommand对象,所以我不会进一步解释。

var dcmd = (DbCommand)cmd.GetType()
                   .GetMethod("CreateCommand", BindingFlags.Instance | BindingFlags.NonPublic)
                   .Invoke(cmd, new[] {translator, identifierValues});

我希望您觉得这篇文章有用。如果您有任何问题或建议,请在评论区告诉我!

© . All rights reserved.