在如上代码片段中我们实现了接口IObserver<KeyValuePair<string, object>>的IObserver<KeyValuePair<string,object>>.OnNext的方法,参数为KeyValuePair<string,object>,其中Key是事件的名称,而Value是一个匿名对象.
运行程序输出结果如下所示:
System.Data.SqlClient.WriteConnectionOpenBefore
{ OperationId = f5f4d4f0-7aa1-46e6-bd48-78acca3dac0a, Operation = OpenAsync, Connection = System.Data.SqlClient.SqlConnection, Timestamp = 1755845041766 }
System.Data.SqlClient.WriteCommandBefore
{ OperationId = 3d8617d1-0317-4f75-bffd-5b0fddf5cc12, Operation = ExecuteReaderAsync, ConnectionId = 554f4ee4-47c3-44ff-a967-cc343d1d5019, Command = System.Data.SqlClient.SqlCommand }
System.Data.SqlClient.WriteConnectionOpenAfter
{ OperationId = f5f4d4f0-7aa1-46e6-bd48-78acca3dac0a, Operation = OpenAsync, ConnectionId = 554f4ee4-47c3-44ff-a967-cc343d1d5019, Connection = System.Data.SqlClient.SqlConnection, Statistics = System.Data.SqlClient.SqlStatistics+StatisticsDictionary, Timestamp = 1755851869508 }
System.Data.SqlClient.WriteCommandAfter
{ OperationId = 3d8617d1-0317-4f75-bffd-5b0fddf5cc12, Operation = ExecuteReaderAsync, ConnectionId = 554f4ee4-47c3-44ff-a967-cc343d1d5019, Command = System.Data.SqlClient.SqlCommand, Statistics = System.Data.SqlClient.SqlStatistics+StatisticsDictionary, Timestamp = 1755853467664 }
System.Data.SqlClient.WriteConnectionCloseBefore
{ OperationId = ed240163-c43a-4394-aa2d-3fede4b27488, Operation = Close, ConnectionId = 554f4ee4-47c3-44ff-a967-cc343d1d5019, Connection = System.Data.SqlClient.SqlConnection, Statistics = System.Data.SqlClient.SqlStatistics+StatisticsDictionary, Timestamp = 1755854169373 }
System.Data.SqlClient.WriteConnectionCloseAfter
{ OperationId = ed240163-c43a-4394-aa2d-3fede4b27488, Operation = Close, ConnectionId = 554f4ee4-47c3-44ff-a967-cc343d1d5019, Connection = System.Data.SqlClient.SqlConnection, Statistics = System.Data.SqlClient.SqlStatistics+StatisticsDictionary, Timestamp = 1755854291040 }
42
如上结果可以清楚的看到里面存在6个事件,我们可以看到两个是在打开数据库之前和之后执行的,两个是在执行命令之前和之后执行的,还有两个是在关闭数据库连接之前和之后执行的。
另外可以看到每个事件中都包含一组参数,如OperationId、Operation、ConnectionId等,这些参数通常作为匿名对象属性传输,我们可以通过反射来获取这些属性的类型化的值。
现在我们解决了我们最初的需求,获取数据库中所有查询的执行时间,并将其输出到控制台中,我们需要进行修改,代码如下所示:
private readonly AsyncLocal<Stopwatch> _stopwatch = new AsyncLocal<Stopwatch>(); private void Write(string name, object value) { switch (name) { case "System.Data.SqlClient.WriteCommandBefore": { _stopwatch.Value = Stopwatch.StartNew(); break; } case "System.Data.SqlClient.WriteCommandAfter": { var stopwatch = _stopwatch.Value; stopwatch.Stop(); var command = GetProperty<SqlCommand>(value, "Command"); Console.WriteLine($"CommandText: {command.CommandText}"); Console.WriteLine($"Elapsed: {stopwatch.Elapsed}"); Console.WriteLine(); break; } } } private static T GetProperty<T>(object value, string name) { return (T)value.GetType() .GetProperty(name) .GetValue(value); }
在这我们将拦截数据库中查询的开始和结束事件,在执行之前我们创建并且启动stopwatch,将其存储在AsyncLocal<stopwatch>中,以后面将其返回,在执行完成后,我们获取之前启动的stopwatch,停止它,通过反射从参数值中获取执行命令,并将结果输出到控制台。
执行结果如下所示:
CommandText: SELECT 42;
Elapsed: 00:00:00.1509086
42
现在我们已经解决了我们的需求,但是目前还存在一个小的问题,当我们订阅事件diagnosticListener时,我们从它里面将接收到所有的事件,包括我们不需要的事件,但是呢发送的每个事件都会创建一个带有参数的匿名对象,这会在GC上造成额外的压力。
我们需要解决如上的问题,避免我们去处理所有的事件,我们需要指定Predicate<string>这个特殊的委托类型,我们声明IsEnabled方法,在此筛选对应名称的消费者。
下面我们修改一下方法IObserver<DiagnosticListener>.OnNext
public void OnNext(DiagnosticListener value) { if (value.Name == "SqlClientDiagnosticListener") { var subscription = value.Subscribe(this, IsEnabled); _subscriptions.Add(subscription); } } private bool IsEnabled(string name) { return name == "System.Data.SqlClient.WriteCommandBefore" || name == "System.Data.SqlClient.WriteCommandAfter"; }
现在我们只会对事件System.Data.SqlClient.WriteCommandBefore和System.Data.SqlClient.WriteCommandAfter调用Write方法。
使用Microsoft.Extensions.DiagnosticAdapter