使用 WCF 和 DbProviderFactory 注入的 DbProvider Profiler:IV 的第 II 部分






4.33/5 (4投票s)
在我们的 ProxyDbxxx 类中创建分析信息。
引言
在专业项目中,我们使用 Entity Framework 作为应用程序的 ORM 提供程序。在许多情况下,我们想知道某个 LINQ 查询生成了什么 SQL 查询。这通常发生在某个特定查询运行缓慢、返回意外结果等情况下。为了“调试” LINQ 查询,我们可以使用 VS 2010(Ultimate 版)的强大新功能 IntelliTrace。通过此工具窗口,我们可以检查在给定时间点运行了哪个查询。如果我们有 VS2010 的 Ultimate 许可证,这会很棒,但可惜我们没有。
IntelliTrace 的第二个缺点是它没有告诉我们执行查询的性能。它运行花了 500 毫秒,还是 5 毫秒?我们无从得知。
等一下!为什么不使用 SQL Profiler 软件呢?嗯,我建议你们都尝试使用 SQL-Profiler 来调试 EF 查询。这简直是令人讨厌的(pain in the ****)。
因此,我决定自己寻找一个解决方案,我尽量保持它尽可能基础和简单,所以不要在这里寻找过度工程!
这将是一系列文章,我会尝试在这里维护一个索引,以便您更容易找到不同的部分。
目录
- 第四部分之二:重写我们自己的代理的默认 DbProvider 实现
- 第四部分之二:在我们的 ProxyDbxxx 类中创建分析信息
- 第四部分之三:将我们的代码连接到外部世界:WCF
- 第四部分之四:创建一个简单的 VS 工具窗口来显示分析输出
创建代理类
首先,如果我们想真正分析任何东西,我们需要更多信息。因此,以下类将被包装在代理中
DbCommand
DbConnection
DbDataAdapter
DbCommandDefinition
这些类,特别是 DbConnection
和 DbCommand
,将为我们提供足够的信息来开始分析发送到 SQL 后端的操作。我们始终会将在我们的包装类中保留对被代理对象的引用,以便我们可以将方法调用、属性 getter/setter 转发到原始实现。我们只会添加一些仪器来测量执行某事所花费的时间。我们还将为每个新的 DbConnection
分配一个 GUID
。这是“转发”或代理机制如何工作的示例
这是 ProxyDbConnection
的构造函数的样子
public class ProxyDbConnection : DbConnection
{
public DbProviderFactory ProxiedFactory { get; private set; }
public DbConnection ProxiedConnection { get; private set; }
public string Guid { get; private set; }
public ProxyDbConnection(DbConnection proxiedConnection,
DbProviderFactory proxiedFactory)
{
ProxiedConnection = proxiedConnection;
ProxiedFactory = proxiedFactory;
Guid = System.Guid.NewGuid().ToString();
}
...
}
对于属性
public override ConnectionState State
{
get { return ProxiedConnection.State; }
}
对于方法
public override void Open()
{
ProxiedConnection.Open();
}
下面您可以看到这些代理类的类图

初始分析“库”
我创建了一个包含三个方法的接口:IProfilerOutput
。

此接口的目的是允许不同的媒体输出我们的分析统计信息。此接口的第一个实现会将所有分析信息定向到 Visual Studio 的 Output 工具窗口。这通过使用 Debug.Write(Line)
方法非常简单地完成。在我继续之前,我应该注意的是,我已经创建了用于通过 WCF 服务传播分析信息所需的 WCF 合同。我在 OutputWindow
实现中使用了这些 DataContracts
来使用 ToString()
方法。
供您参考,这是 WCF 数据合同的类图

这是 DebugProfileOutput
类的完整实现
public class DebugProfileOutput: IProfileOutput
{
#region Implementation of IProfileOutput
public void CommandExecute(ProxyDbCommand command, TimeSpan duration,
ExecuteMethod method, List<QueryExecutionPlan> executionPlan,
object result)
{
var commandMessage = new Van.Parys.Data.Common.CommandMessage(
command.CommandText, duration, method.ToString(),
command.ProxyDbConnection.Guid)
{QueryExecutionPlans = executionPlan, NonReaderResult = result};
Debug.WriteLine(commandMessage.ToString());
}
public void ContextAttached(string contextName)
{
Debug.WriteLine(string.Format("{0} Attached!", contextName));
}
public void ConnectionChanged(ProxyDbConnection connection)
{
var connectionMessage = new ConnectionMessage() {
ConnectionGUID = connection.Guid,
Open = connection.State == ConnectionState.Open};
Debug.WriteLine(connectionMessage.ToString());
}
#endregion
}
为了测量某个操作花费的时间,我使用了以下非常有用的实用方法
public static class ProfilerStopwatchAction
{
public static TimeSpan Measure(Action action)
{
var startNew = Stopwatch.StartNew();
//perform action to measure time
action();
startNew.Stop();
return TimeSpan.FromMilliseconds(startNew.ElapsedMilliseconds);
}
}
用法
...
TimeSpan duration = ProfilerStopwatchAction.Measure(() => 1 + 1);
...
分析会话的设置和配置
还记得初始化我们的分析库所需的初始代码吗?猜不着了吧!嗯,它是这样的
/// <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());
}
嗯,事情将变得复杂两倍!这是因为我向 Init()
函数添加了一个可选参数
public static void Init(ProfilingOptions options = null)
ProfilingOptions
是一个定义以下选项的类
bool IncludeQueryStatistics
- 这使我们能够将查询执行计划包含在分析输出中。我们都知道在 SQL Management Studio 中获得的图形化输出,嗯,这就是文本表示。
- 请注意:这不适用于所有 SQL 提供程序!此功能使用“
SET SHOWPLAN_ALL ON
”命令,该命令对例如 SqlCe 无效!
bool ProfileConnections
- 指示我们是否关心分析连接状态(打开/关闭)
ProfilingOutput ProfilingOutput
ProfilingOutput
是一个Enum
,它告诉我们使用哪个IProfileOutput
实现。目前,这里唯一的选项是“Debug”
public enum ProfilingOutput { Debug }
所以,现在我们可以对哪些内容进行分析,哪些不进行分析,有更多的控制权。这些选项中值得关注的是包含查询执行计划的能力。该计划的检索是通过以下代码实现的
...
private const string SetShowplanAllOn = "SET SHOWPLAN_ALL ON";
private const string SetShowplanAllOff = "SET SHOWPLAN_ALL OFF";
...
public override object ExecuteScalar()
{
var executionPlan = ProfileQueryExecutionPlan();
object executeScalar = null;
var timeSpan = ProfilerStopwatchAction.Measure(() =>
executeScalar = ProxiedCommand.ExecuteScalar());
EFProfiler.CommandExecute(this, timeSpan, ExecuteMethod.ExecuteScalar,
executionPlan, executeScalar);
return executeScalar;
}
...
internal List<QueryExecutionPlan> ProfileQueryExecutionPlan()
{
if (!ProxyGenerator.Options.IncludeQueryStatistics)
return null;
var executionPlan = new List<QueryExecutionPlan>();
if (!profilingQuery)
{
profilingQuery = true;
var cmd = ProxyDbConnection.CreateCommand() as ProxyDbCommand;
cmd.Connection = this.ProxyDbConnection;
cmd.CommandText = SetShowplanAllOn;
cmd.CommandType = CommandType.Text;
cmd.ProxiedCommand.ExecuteNonQuery();
cmd.CommandText = this.CommandText;
cmd.CommandType = CommandType.Text;
var statsReader = cmd.ProxiedCommand.ExecuteReader();
while (statsReader.Read())
{
executionPlan.Add(ParseReader(statsReader));
}
statsReader.Close();
cmd.CommandText = SetShowplanAllOff;
cmd.CommandType = CommandType.Text;
cmd.ProxiedCommand.ExecuteNonQuery();
profilingQuery = false;
}
return executionPlan;
}
在此特定场景中,一个陷阱是避免在 IncludeQueryStatistics
设置为 true
时出现无限循环。发生这种无限循环是因为 ProfileQueryExecutionPlan
方法本身执行三个查询。因此,我们必须避免对这三个查询也进行分析。正如您在上面的示例中所见,这是通过 bool profilingQuery
实现的。
分析 ProxyDbCommand
最后,我们可以开始分析一些东西了!演示应用程序包含在源文件中。这是测试代码
static void Main(string[] args)
{
//single point of 'intrusion'
ProxyGenerator.Init(new ProfilingOptions() {IncludeQueryStatistics = true,
ProfilingOutput = ProfilingOutput.Debug});
Debug.WriteLine("Test Count method");
var count = new DemoDataEntities().Person.Count();
Debug.WriteLine("Test Where Take(1)");
var queryable = new DemoDataEntities().Person.Where(
x => x.Name.StartsWith("P")).Take(1).ToList();
}
这是输出窗口的快照
Test Count method
a5454996-dd65-4537-97c6-55a0be329837 changed to Open
ExecuteDbDataReader: [1ms] [result:'-1'] :
SELECT
[GroupBy1].[A1] AS [C1]
FROM ( SELECT
COUNT(1) AS [A1]
FROM [Person] AS [Extent1]
) AS [GroupBy1]
a5454996-dd65-4537-97c6-55a0be329837 changed to Closed
Test Where Take(1)
5247999a-7d94-43fb-82bd-7e0765244405 changed to Open
ExecuteDbDataReader: [0ms] [result:'-1'] :
SELECT TOP (1)
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
[Extent1].[Function] AS [Function],
[Extent1].[Description] AS [Description]
FROM [Person] AS [Extent1]
WHERE (CHARINDEX(N'P', [Extent1].[Name])) = 1
5247999a-7d94-43fb-82bd-7e0765244405 changed to Closed
结论
在本文中,我们为我们的库添加了一些实际的分析信息。请随意在您自己的 EF 项目等中尝试。在下一篇文章中,我将探讨如何使用 WCF 服务将分析器公开到外部世界。
下一部分
下一篇文章将介绍创建一个 WCF 客户端/服务器设置,以使外部侦听器能够处理分析信息。