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






4.86/5 (5投票s)
解释使用代理类分析 DbProvider 的原理。
引言
在一个专业项目中,我们使用 Entity Framework 作为应用程序的 ORM 提供程序。有好几次我们想知道某个 LINQ 查询生成的 SQL 语句是什么。这通常发生在某个查询非常慢,返回意外结果等情况下。要“调试” LINQ 查询,我们可以使用 VS 2010(Ultimate)的酷炫新功能 IntelliTrace。使用这个工具窗口,我们可以检查在特定时间点运行了哪个查询。如果我们有 VS2010 的 Ultimate 许可证,这会很棒,但很可惜我们没有。
IntelliTrace 的第二个缺点是它并没有告诉我们执行查询的性能。是花了 500 毫秒运行,还是 5 毫秒?我们不知道。
等等!为什么不直接使用 SQL Profiler 软件呢?嗯,我建议你们都尝试使用 SQL-Profiler 来调试 EF 查询。这太他妈麻烦了。
所以,我决定自己寻找解决方案。我尽量保持它最基础、最简单,所以不要在这里寻找过度工程化!
这将是一个系列文章,我会在这里维护一个索引,以便您可以轻松找到不同的部分。
目录
- 四部曲之一:用我们自己的代理类覆盖默认的 DbProvider 实现
- 四部曲之二:在我们的 ProxyDbxxx 类中创建分析信息
- 四部曲之三:将我们的代码连接到外部世界:WCF
- 四部曲之四:创建一个简单的 VS 工具窗口来显示分析输出
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` 事件显示信息性消息。稍后,当我们讨论创建分析系统的策略时,我们将加入真实的统计数据。所以现在,只有调试消息。下载中包含了一个示例程序,其输出将是这样的。
在下面的图片中,您可以看到 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
...
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 框架为密封类创建代理?
正如您所见,这个系列文章对我来说也是一次冒险,所以欢迎任何关于该主题的想法!