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

使用 WCF 和 DbProviderFactory 注入进行 DbProvider 剖析:第 1 部分(共 4 部分)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (5投票s)

2011 年 9 月 8 日

CPOL

8分钟阅读

viewsIcon

28194

downloadIcon

284

解释使用代理类分析 DbProvider 的原理。

引言

在一个专业项目中,我们使用 Entity Framework 作为应用程序的 ORM 提供程序。有好几次我们想知道某个 LINQ 查询生成的 SQL 语句是什么。这通常发生在某个查询非常慢,返回意外结果等情况下。要“调试” LINQ 查询,我们可以使用 VS 2010(Ultimate)的酷炫新功能 IntelliTrace。使用这个工具窗口,我们可以检查在特定时间点运行了哪个查询。如果我们有 VS2010 的 Ultimate 许可证,这会很棒,但很可惜我们没有。

IntelliTrace 的第二个缺点是它并没有告诉我们执行查询的性能。是花了 500 毫秒运行,还是 5 毫秒?我们不知道。

等等!为什么不直接使用 SQL Profiler 软件呢?嗯,我建议你们都尝试使用 SQL-Profiler 来调试 EF 查询。这太他妈麻烦了。

所以,我决定自己寻找解决方案。我尽量保持它最基础、最简单,所以不要在这里寻找过度工程化!

这将是一个系列文章,我会在这里维护一个索引,以便您可以轻松找到不同的部分。

目录

Using the Code

使用代码就像这样简单

/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
    //First and only entry point of the profiler
    ProxyGenerator.Init();
            
    Application.Run(new Form1());
}

正如你所见,这几乎不会影响你的应用程序的设计方式等。这使得该分析器非常适合集成到现有项目中。在第一篇文章中,我们将只使用一个事件来捕获所有分析信息,这最终会通过实现一个 WCF 服务来改变,用于服务器(ProxyDbxxx 等)和客户端(VS2010 插件)之间的通信。但现在,我们可以使用这些命令捕获所有调试信息。

public void SomeMethod()
{
    ProxyGenerator.DbProviderInfo += 
      new EventHandler<DbProviderInfoEventArgs>(ProxyGenerator_DbProviderInfo);
}

void ProxyGenerator_DbProviderInfo(object sender, DbProviderInfoEventArgs e)
{
    MessageBox.Show(e.Message);
}

目前,我们只通过 `DbProviderInfo` 事件显示信息性消息。稍后,当我们讨论创建分析系统的策略时,我们将加入真实的统计数据。所以现在,只有调试消息。下载中包含了一个示例程序,其输出将是这样的。

ConsoleOutput.png

在下面的图片中,您可以看到 ProxyGenerator 的类图。

接下来,我将讨论创建代理 DbProvider 的技术,以及如何使用它们来拦截对 `DbCommand`、`DbConnection` 等类的所有方法调用。

DbProviderFactory

首先,快速介绍一下 `DbProviderFactory`(MSDN 概念)。这个“工厂模式”实现旨在将所有数据库访问从代码中抽象出来。每个“大型”数据库提供程序(MSSQL、Oracle 等)都有一个 `DbProviderFactory` 的实现。`DbProviderFactory` 本身是一个抽象类,其中包含了连接和查询任何数据库所需的一切。所有 `DbProviderFactory` 的实现都由 .NET Framework 存储在 `DbProviderFactories` 类中。通过使用这个类,我们可以执行以下代码:

DbProviderFactory factory = DbProviderFactories.GetFactory(providerName);

这将为您提供一个匹配指定 providerName 的 `DbProviderFactory` 实例。通过使用这个 `DbProviderFactory` 模式,不需要对 MSSQL、Oracle 特定实现产生依赖。

这些实现是如何存储的?

`DbProviderFactories` 类包含三个公共成员,其中一个对我们特别有吸引力:“`GetFactoryClasses`”。这个方法返回一个 `DataTable`,其中包含已加载程序集中的所有 `DbProviderFactory` 实现。我首先想到的是,我能否编辑这个 `DataTable`,以便为提供程序提供一个自定义实现类?通过使用公共方法 `GetFactoryClasses`,我们只能访问存储在 `DbProviderFactories` 类内存中的 `DataTable` 的副本。为了访问原始 `DataTable`,我使用了一个反汇编器来检查 .NET Framework 类。我注意到有一个 `private static method` 返回了 `DbProviderFactories` 类创建的原始 `DataTable` 的引用。

找到一种方法来访问包含 ProviderNames 和 AQN [^] 组合的原始 `DataTable` 是此方法成功的关键。这可能看起来不对,但正如我的一位朋友所说,“傻一点没什么错”。

//DbProviderFactories has a private static method
//(thanks jetBrains decompiler!) which gives us
//a non copy of the available DbProviderFactories
var dbProvidersFactoriesDataTable = typeof(DbProviderFactories).GetMethod(
  "GetProviderTable", 
  BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, null) 
  as DataTable;

这是一个关于默认 `DataTable` 可能是什么样子的图示:

覆盖默认的 DbProviderFactory 实现

基本上有两种方法可以做到这一点。IOC [^](`Castle`,`Lin.Fu`)方式创建代理类,并将原始 TypeName 替换为生成的代理类型的名称。我很想这样做,但我就是找不到(好的)方法来代理一个密封类。如果有人知道方法,请告诉我!

第二种方法是自己创建代理类。在 .NET 1.0 时代,这意味着对于每一个可能的 DbProvider,我们都必须编写一个代理版本(ProxyOracle`DbProviderFactory` 等)。幸运的是,我们自 .NET 2.0 起就有了泛型!所以我们可以编写一个类,如下所示:

ProxyDbProviderFactory<T> where T : DbProviderFactory

简单,却非常强大!为什么它强大?嗯,我们可以将它用于现在或将来可能制作的几乎所有 `DbProviderFactory`(天哪,这词写起来太长了)。下面的代码片段演示了如何使用这个泛型类型创建一个新类型。

var DbProviderFactoryTypes = 
  dbProvidersFactoriesDataTable.Rows.OfType<datarow>().Select(
  x => x["InvariantName"].ToString()).ToList();

foreach (string providerInvariantName in DbProviderFactoryTypes)
{
    //get an instance of the DbProviderFactory in this loop.
    var factory = DbProviderFactories.GetFactory(providerInvariantName);
        
    //create new generic type ProxyDbProviderFactory<{TypeOfFactoryToProxy}>
    var factoryType = typeof(ProxyDbProviderFactory<>).MakeGenericType(
                      new Type[] { oringalFactoryType });

    //call static method Initialize to construct the instance
    factoryType.GetMethod("Initialize", BindingFlags.FlattenHierarchy | 
                BindingFlags.Public | BindingFlags.Static).Invoke(null, null);
    
    ... (see below)
}

在我们继续之前,您应该注意到所有 `DbProviderFactory` 实现都必须使用单例模式。在这种情况下,这意味着所有 Factory 都有一个静态字段 '`Instance`',它包含该类型的唯一实例。了解这一点非常有益,因为我们必须遵循此模式。正如我们在上一个代码片段的最后一行代码中所见:

//call static method Initialize to construct the instance
factoryType.GetMethod("Initialize", BindingFlags.FlattenHierarchy | 
            BindingFlags.Public | BindingFlags.Static).Invoke(null, null);

我们调用静态(这就是为什么 `Invoke` 方法是用双 `null` 参数调用的)方法 `Initialize` 来设置新创建的类型。这是 `Initialize` 方法:

...
//static field containing the singleton instance
public static ProxyDbProviderFactory<tfac> Instance;
...
public static void Initialize()
{
    Instance = new ProxyDbProviderFactory<tfac>();
}
...
public ProxyDbProviderFactory()
{
    //access the singleton instance of the type that is proxied
    FieldInfo field = typeof(TFac).GetField("Instance", 
                      BindingFlags.Public | BindingFlags.Static);
    this.ProxiedDbProviderFactory = (TFac)field.GetValue(null);
}

在这个 `Initialize` 方法中,我们实例化了我们代理的 `DbProviderFactory` 的单例实例。在上面的代码片段中,我们还看到了构造函数,它提取了代理类型的 `Instance` 字段。我们将其命名为 `ProxiedDbProviderFactory` 并存储一个对该实例的引用。那么,代理的 `DbProviderFactory`(例如,Oracle`DbProviderFactory`)何时被初始化(=创建单例实例)?这发生在这一行代码中:

//get an instance of the DbProviderFactory in this loop.
var factory = DbProviderFactories.GetFactory(providerInvariantName);

通过获取一个 factory,我们确保它将被正确初始化。在程序的其余部分,我们实际上不需要这个变量 '`factory`'。

接下来是实际覆盖前面讨论的 `DataTable` 中的类型。下面的代码片段演示了如何覆盖 `DbProviderFactories` 的默认实现。请注意,`DataTable` 的列是只读的,所以我必须手动覆盖它来替换字段。请看下面的示例:

foreach (string providerInvariantName in DbProviderFactoryTypes)
{
    ... (see above)
    
    //retrieve the datarow in which we will alter
    //the Type to the newly contructed ProxyType
    var dataRowToEdit = 
        dbProvidersFactoriesDataTable.Rows.OfType<datarow>().FirstOrDefault(
        dt => ((string) dt["InvariantName"]) == providerInvariantName);

    if (dataRowToEdit == null)
        return;

    //set readOnly false to edit table
    dbProvidersFactoriesDataTable.Columns["AssemblyQualifiedName"].ReadOnly = false;

    //map the DbProvider to the new proxy
    dataRowToEdit["AssemblyQualifiedName"] = factoryType.AssemblyQualifiedName;

    //set readOnly back to original
    dbProvidersFactoriesDataTable.Columns["AssemblyQualifiedName"].ReadOnly = true;
}

执行此代码后,`DataTable` 将如下所示:

为了结束第一部分,我们现在只需要在 `ProxyDbProviderFactory` 类中实现 `DbProviderFactory` 抽象类的所有必需的覆盖。供参考,我将在此包含一些实现示例:

...
public override DbCommandBuilder CreateCommandBuilder()
{
    return ProxiedDbProvider.CreateCommandBuilder();
}

public override DbConnection CreateConnection()
{
    return ProxiedDbProvider.CreateConnection();
}

public override DbConnectionStringBuilder CreateConnectionStringBuilder()
{
    return ProxiedDbProvider.CreateConnectionStringBuilder();
}
...

显然,我们所做的就是调用代理实例的成员,因此所有默认功能都得以保留。稍后,我们还将创建 `DbConnection`、`DbCommand` 等类的代理类,以实现真正的分析。

IServiceProvider 接口

最后一件事情,我们应该简要讨论的是 `IServiceProvider` 接口的实现。所有 `DbProviderFactory` 实现也实现了 `IServiceProvider`。根据 MSDN,这个接口“定义了一种检索服务对象(即,为其他对象提供自定义支持的对象)的机制”。在调试和进行网络研究时,我发现唯一被请求的服务是 `DbProviderServices` 类型。为了支持这个 `IServiceProvider` 接口,我们按如下方式实现服务方法:

public object GetService(Type serviceType)
{
    ProxyGenerator.OnQueryExecuted(string.Format(
         "GetService requested {0}", serviceType.Name));

    if (serviceType == GetType())
    {
        return ProxiedDbProvider;
    }

    DbProviderServices service;

    if (serviceType == typeof(DbProviderServices))
    {
        if (ProxiedDbProvider is IServiceProvider)
        {
            service = (ProxiedDbProvider as IServiceProvider).GetService(serviceType) 
                       as DbProviderServices;
        }
        else
        {
            service = null;
        }
    }

    if (service != null)
    {
        return new ProxyDbProviderServices(service);
    }

    return null;
}

正如您所见,我们已经将代理的 `DbProviderFactory GetService`() 方法的默认返回值封装在一个名为 `ProxyDbProviderServices` 的新类中。这使我们能够对基类中发生的情况进行一些额外的初步日志记录。

此方法的缺点

  • 使用 .NET Framework 的未记录功能(`private DataTable`)

此方法的优点

  • 无需自定义 `ObjectContext` / `DbContext`(Entity Framework)。
  • 无需为每种 SQL 技术自定义 DbProvider。
  • “单点注入”=> 一行代码即可让库访问运行时。
  • 覆盖所有 `DbProviderFactory` 的实现者,甚至是奇特的自定义实现。

总结

那么,在第一部分之后我们做了什么?总而言之,这个总结:

  • 获取包含所有 `DbProviderFactory` 实现者的内部使用的 `DataTable` 的引用
  • 创建一个泛型代理类型来封装默认的 `DbProviderFactory` 类型
  • 在 `ProxyDbProviderFactory` 中实现 `DbProviderFactory` 的抽象方法

下一步

在本文系列的下一篇文章中,我将讨论如何实现 `DbConnection`、`DbCommand` 等的代理类。此外,我们将讨论如何分析执行的查询。

给读者的问答

您认为这种修改内部定义的 `DataTable` 以更改哪些 DbProviders 被初始化和使用的机制如何?

您是否看到了此方法的其他缺点?

是否有方法可以使用 Castle、Unity、Lin.Fu 等 IOC 框架为密封类创建代理?

正如您所见,这个系列文章对我来说也是一次冒险,所以欢迎任何关于该主题的想法!

© . All rights reserved.