实体框架 6 - 计时查询

本文关键字:查询 框架 实体 | 更新日期: 2023-09-27 18:32:33

我正在使用实体框架 6,它是很棒的数据库拦截器功能来记录从应用程序数据库发送的查询。但是,我正在努力计时这些查询,我有一个长时间运行的查询,它返回数十万到数百万行,因此大约需要 6 到 15 秒,具体取决于此查询将返回的数据量。实体框架返回一个 SqlDataReader,因此我无法获得获得结果所需的确切时间。我想知道从发送查询到读取最后一行的完整执行时间。有没有办法我能做到。

实体框架 6 - 计时查询

这是我通常用于 EF 的记录器。

public class EFLoggerForTesting : IDbCommandInterceptor
{
    static readonly ConcurrentDictionary<DbCommand, DateTime> m_StartTime = new ConcurrentDictionary<DbCommand, DateTime>();
    public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        Log(command, interceptionContext);
    }
    public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
    {
        Log(command, interceptionContext);
    }
    public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    {
        Log(command, interceptionContext);
    }
    private static void Log<T>(DbCommand command, DbCommandInterceptionContext<T> interceptionContext)
    {
        DateTime startTime;
        TimeSpan duration;

        if (m_StartTime.TryRemove(command, out startTime))
        {
            duration = DateTime.Now - startTime;
        }
        else
            duration = TimeSpan.Zero;
        var requestId =-1;
        string message;
        var parameters = new StringBuilder();
        foreach (DbParameter param in command.Parameters)
        {
            parameters.AppendLine(param.ParameterName + " " + param.DbType + " = " + param.Value);
        }
        if (interceptionContext.Exception == null)
        {
            message = string.Format("Database call took {0} sec. RequestId {1} 'r'nCommand:'r'n{2}", duration.TotalSeconds.ToString("N3"), requestId, parameters.ToString() + command.CommandText);
        }
        else
        {
            message = string.Format("EF Database call failed after {0} sec. RequestId {1} 'r'nCommand:'r'n{2}'r'nError:{3} ", duration.TotalSeconds.ToString("N3"), requestId, parameters.ToString() + command.CommandText, interceptionContext.Exception);
        }
        Debug.WriteLine(message);
    }

    public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
    {
        OnStart(command);
    }
    public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        OnStart(command);
    }
    public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    {
        OnStart(command);
    }
    private static void OnStart(DbCommand command)
    {
        m_StartTime.TryAdd(command, DateTime.Now);
    }
}

不幸的是,该文档不存在,所以我不知道这是否适用于您的方案。

如果你在Web应用程序中工作,你可以尝试Glimpse:http://getglimpse.com/。否则,请尝试 MiniProfiler: http://miniprofiler.com/。

您可以尝试此处解释的示例

在此示例中,我们在命令开始执行时启动Stopwatch,并在命令完成后停止Stopwatch,这样我们就可以识别慢查询并记录它们。

public class SqlMonitorInterceptor : IDbCommandInterceptor
{
    private static readonly ILog logger = LogManager.GetCurrentClassLogger();
    private static readonly int sqlWarningThresholdMs = int.Parse(ConfigurationManager.AppSettings["sqlPerformance_warningThresholdMilliseconds"]);
    private readonly Stopwatch _stopwatch = new Stopwatch();
    public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
    {
        CommandExecuting();
    }
    public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
    {
        CommandExecuted(command, interceptionContext);
    }
    public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        CommandExecuting();
    }
    public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        CommandExecuted(command, interceptionContext);
    }
    public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    {
        CommandExecuting();
    }
    public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    {
        CommandExecuted(command, interceptionContext);
    }
    private void CommandExecuting() {
        _stopwatch.Restart();
    }
    private void CommandExecuted<TResult>(DbCommand command, DbCommandInterceptionContext<TResult> interceptionContext)
    {
        _stopwatch.Stop();
        LogIfError(command, interceptionContext);
        LogIfTooSlow(command, _stopwatch.Elapsed);
    }
    private void LogIfError<TResult>(DbCommand command, DbCommandInterceptionContext<TResult> interceptionContext)
    {
        if (interceptionContext.Exception != null)
        {
            logger.ErrorFormat("Command {0} failed with exception {1}",
                command.CommandText, interceptionContext.Exception);
        }
    }
    private void LogIfTooSlow(DbCommand command, TimeSpan completionTime)
    {
        if (completionTime.TotalMilliseconds > sqlWarningThresholdMs)
        {
            logger.WarnFormat("Query time ({0}ms) exceeded the threshold of {1}ms. Command: '"{2}'"",
                completionTime.TotalMilliseconds, sqlWarningThresholdMs, command.CommandText);
        }
    }
}

正如佩雷斯@Ricardo回答所暗示的那样,Glimpse对此有好处。它带有Glimpse.Ado插件,可用于轻松分析任何DbConnection,这是此处要扩展的主要类。

Glimpse 中 ADO 的手动集成点是将DbConnection包装在GlimpseDbConnection中,如这篇博文所示:http://getglimpse.com/Docs/Manual-ADO-Integration。EF 等其他提供程序会自动与Glimpse.Ef包集成。

如果您仍然决定要手动实现此目的,我建议您自己包装DbConneciton并使用它来代替常规DbConnection。这样做可以得到简单的时间。

你可以在他们的github上看到Glimpse是如何做到的:https://github.com/Glimpse/Glimpse/tree/master/source/Glimpse.Ado/AlternateType

或者,始终可以选择在存储库级别或方法级别添加此类日志记录,具体取决于所需的日志记录量。

Johnathan 接受答案的 EF Core 版本,适用于任何搜索:

using Microsoft.EntityFrameworkCore.Diagnostics;
using System.Data.Common;
public class EFLogger: DbCommandInterceptor
{
    private static readonly ConcurrentDictionary<Guid, DateTime> _startTimes = new ConcurrentDictionary<Guid, DateTime>();
    public override DbDataReader ReaderExecuted(DbCommand command, CommandExecutedEventData eventData, DbDataReader reader)
    {
        Log(command, eventData);
        return reader;
    }
    public override int NonQueryExecuted(DbCommand command, CommandExecutedEventData eventData, int result)
    {
        Log(command, eventData);
        return result;
    }
    public override object ScalarExecuted(DbCommand command, CommandExecutedEventData eventData, object result)
    {
        Log(command, eventData);
        return result;
    }
    public override void CommandFailed(DbCommand command, CommandErrorEventData eventData)
    {
        Log(command, eventData);
    }
    private static void Log(DbCommand command, CommandEventData eventData)
    {
        TimeSpan? duration = null;
        if (_startTimes.TryRemove(eventData.CommandId, out DateTime startTime))
            duration = DateTime.Now - startTime;
        var parameters = new StringBuilder();
        foreach (DbParameter param in command.Parameters)
        {
            parameters.AppendLine(param.ParameterName + " " + param.DbType + " = " + param.Value);
        }
        string message = $"Database call {(eventData is CommandErrorEventData ? "FAILED" : "succeeded")} in {duration?.TotalMilliseconds ?? -1:N3} ms. CommandId {eventData.CommandId} 'r'nCommand:'r'n{parameters}{command.CommandText}";
        Console.WriteLine(message);
    }
    public override InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<int> result)
    {
        OnStart(eventData.CommandId);
        return result;
    }
    public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
    {
        OnStart(eventData.CommandId);
        return result;
    }
    public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<object> result)
    {
        OnStart(eventData.CommandId);
        return result;
    }
    private void OnStart(Guid commandId)
    {
        _startTimes.TryAdd(commandId, DateTime.Now);
    }
}

下面是我对原始DatabaseLogFormatter的简化版本。主要区别在于这个没有过滤,我不记录实际的SQL查询或其参数(因为我只对查询计时感兴趣(。它会记录何时打开连接、执行查询以及何时再次关闭连接。正如上面@aske-b所指出的那样,使用DateTime(正如公认的答案一样(并不是很准确。


public class CustomDatabaseLogFormatter : IDbCommandInterceptor, IDbConnectionInterceptor
{
    private readonly Action<string> _writeAction;
    private readonly Stopwatch _stopwatch = new Stopwatch();
    /// <summary>
    /// Creates a formatter that will log every command from any context and also commands that do not originate from a context.
    /// </summary>
    /// <remarks>
    /// This constructor is not used when a delegate is set on <see cref="Database.Log" />. Instead it can be
    /// used by setting the formatter directly using <see cref="DbInterception.Add" />.
    /// </remarks>
    /// <param name="writeAction">The delegate to which output will be sent.</param>
    public CustomDatabaseLogFormatter(Action<string> writeAction)
    {
        Check.NotNull(writeAction, "writeAction");
        _writeAction = writeAction;
    }
    internal Action<string> WriteAction
    {
        get { return _writeAction; }
    }
    /// <summary>
    /// Writes the given string to the underlying write delegate.
    /// </summary>
    /// <param name="output">The string to write.</param>
    protected virtual void Write(string output)
    {
        _writeAction(output);
    }
    /// <summary>
    /// The stopwatch used to time executions. This stopwatch is started at the end of
    /// <see cref="NonQueryExecuting" />, <see cref="ScalarExecuting" />, and <see cref="ReaderExecuting" />
    /// methods and is stopped at the beginning of the <see cref="NonQueryExecuted" />, <see cref="ScalarExecuted" />,
    /// and <see cref="ReaderExecuted" /> methods. If these methods are overridden and the stopwatch is being used
    /// then the overrides should either call the base method or start/stop the stopwatch themselves.
    /// </summary>
    /// <returns>The stopwatch.</returns>
    protected internal Stopwatch Stopwatch
    {
        get { return _stopwatch; }
    }
    private void RestartStopwatch()
    {
        Stopwatch.Restart();
    }
    private void StopStopwatch()
    {
        Stopwatch.Stop();
    }
    #region IDbCommandInterceptor
    /// <summary>
    /// This method is called before a call to <see cref="DbCommand.ExecuteNonQuery" /> or
    /// one of its async counterparts is made.
    /// Starts the stopwatch returned from <see cref="Stopwatch"/>.
    /// </summary>
    /// <param name="command">The command being executed.</param>
    /// <param name="interceptionContext">Contextual information associated with the call.</param>
    public virtual void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
    {
        Check.NotNull(command, "command");
        Check.NotNull(interceptionContext, "interceptionContext");
        RestartStopwatch();
    }
    /// <summary>
    /// This method is called after a call to <see cref="DbCommand.ExecuteNonQuery" /> or
    /// one of its async counterparts is made.
    /// Stops the stopwatch returned from <see cref="Stopwatch"/> and calls <see cref="Executed" />.
    /// </summary>
    /// <param name="command">The command being executed.</param>
    /// <param name="interceptionContext">Contextual information associated with the call.</param>
    public virtual void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
    {
        Check.NotNull(command, "command");
        Check.NotNull(interceptionContext, "interceptionContext");
        StopStopwatch();
        Executed(command, interceptionContext);
    }
    /// <summary>
    /// This method is called before a call to <see cref="DbCommand.ExecuteReader(CommandBehavior)" /> or
    /// one of its async counterparts is made.
    /// Starts the stopwatch returned from <see cref="Stopwatch"/>.
    /// </summary>
    /// <param name="command">The command being executed.</param>
    /// <param name="interceptionContext">Contextual information associated with the call.</param>
    public virtual void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        Check.NotNull(command, "command");
        Check.NotNull(interceptionContext, "interceptionContext");
        RestartStopwatch();
    }
    /// <summary>
    /// This method is called after a call to <see cref="DbCommand.ExecuteReader(CommandBehavior)" /> or
    /// one of its async counterparts is made.
    /// Stops the stopwatch returned from <see cref="Stopwatch"/> and calls <see cref="Executed" />.
    /// </summary>
    /// <param name="command">The command being executed.</param>
    /// <param name="interceptionContext">Contextual information associated with the call.</param>
    public virtual void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        Check.NotNull(command, "command");
        Check.NotNull(interceptionContext, "interceptionContext");
        StopStopwatch();
        Executed(command, interceptionContext);
    }
    /// <summary>
    /// This method is called before a call to <see cref="DbCommand.ExecuteScalar" />  or
    /// one of its async counterparts is made.
    /// Starts the stopwatch returned from <see cref="Stopwatch"/>.
    /// </summary>
    /// <param name="command">The command being executed.</param>
    /// <param name="interceptionContext">Contextual information associated with the call.</param>
    public virtual void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    {
        Check.NotNull(command, "command");
        Check.NotNull(interceptionContext, "interceptionContext");
        RestartStopwatch();
    }
    /// <summary>
    /// This method is called after a call to <see cref="DbCommand.ExecuteScalar" />  or
    /// one of its async counterparts is made.
    /// Stops the stopwatch returned from <see cref="Stopwatch"/> and calls
    /// <see cref="Executed" />.
    /// </summary>
    /// <param name="command">The command being executed.</param>
    /// <param name="interceptionContext">Contextual information associated with the call.</param>
    public virtual void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    {
        Check.NotNull(command, "command");
        Check.NotNull(interceptionContext, "interceptionContext");
        StopStopwatch();
        Executed(command, interceptionContext);
    }
    /// <summary>
    /// Called whenever a command has completed executing. Calls <see cref="LogResult" />.
    /// </summary>
    /// <typeparam name="TResult">The type of the operation's results.</typeparam>
    /// <param name="command">The command that was executed.</param>
    /// <param name="interceptionContext">Contextual information associated with the command.</param>
    public virtual void Executed<TResult>(DbCommand command, DbCommandInterceptionContext<TResult> interceptionContext)
    {
        Check.NotNull(command, "command");
        Check.NotNull(interceptionContext, "interceptionContext");
        LogResult(command, interceptionContext);
    }
    /// <summary>
    /// Called to log the result of executing a command.
    /// </summary>
    /// <typeparam name="TResult">The type of the operation's results.</typeparam>
    /// <param name="command">The command being logged.</param>
    /// <param name="interceptionContext">Contextual information associated with the command.</param>
    public virtual void LogResult<TResult>(DbCommand command, DbCommandInterceptionContext<TResult> interceptionContext)
    {
        Check.NotNull(command, "command");
        Check.NotNull(interceptionContext, "interceptionContext");
        var stopwatch = Stopwatch;
        if (interceptionContext.Exception != null)
        {
            Write(
                String.Format(StringResources.CommandLogFailed, stopwatch.ElapsedMilliseconds, interceptionContext.Exception.Message)
                );
        }
        else if (interceptionContext.TaskStatus.HasFlag(TaskStatus.Canceled))
        {
            Write(String.Format(StringResources.CommandLogCanceled, stopwatch.ElapsedMilliseconds));
        }
        else
        {
            var result = interceptionContext.Result;
            var resultString = (object)result == null
                ? "null"
                : (result is DbDataReader)
                    ? result.GetType().Name
                    : result.ToString();
            Write(String.Format(StringResources.CommandLogComplete, stopwatch.ElapsedMilliseconds, resultString));
        }
    }
    #endregion
    #region IDbConnectionInterceptor
    public void BeginningTransaction(DbConnection connection, BeginTransactionInterceptionContext interceptionContext)
    { }
    public void BeganTransaction(DbConnection connection, BeginTransactionInterceptionContext interceptionContext)
    { }
    public void Closing(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
    { }
    /// <summary>
    /// Called after <see cref="DbConnection.Close" /> is invoked.
    /// </summary>
    /// <param name="connection">The connection that was closed.</param>
    /// <param name="interceptionContext">Contextual information associated with the call.</param>
    public void Closed(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
    {
        Check.NotNull(connection, "connection");
        Check.NotNull(interceptionContext, "interceptionContext");
        if (interceptionContext.Exception != null)
        {
            Write(String.Format(StringResources.ConnectionCloseErrorLog, DateTimeOffset.UtcNow, interceptionContext.Exception.Message));
        }
        else
        {
            Write(String.Format(StringResources.ConnectionClosedLog, DateTimeOffset.UtcNow));
        }
    }
    public void ConnectionStringGetting(DbConnection connection, DbConnectionInterceptionContext<string> interceptionContext)
    { }
    public void ConnectionStringGot(DbConnection connection, DbConnectionInterceptionContext<string> interceptionContext)
    { }
    public void ConnectionStringSetting(DbConnection connection, DbConnectionPropertyInterceptionContext<string> interceptionContext)
    { }
    public void ConnectionStringSet(DbConnection connection, DbConnectionPropertyInterceptionContext<string> interceptionContext)
    { }
    public void ConnectionTimeoutGetting(DbConnection connection, DbConnectionInterceptionContext<int> interceptionContext)
    { }
    public void ConnectionTimeoutGot(DbConnection connection, DbConnectionInterceptionContext<int> interceptionContext)
    { }
    public void DatabaseGetting(DbConnection connection, DbConnectionInterceptionContext<string> interceptionContext)
    { }
    public void DatabaseGot(DbConnection connection, DbConnectionInterceptionContext<string> interceptionContext)
    { }
    public void DataSourceGetting(DbConnection connection, DbConnectionInterceptionContext<string> interceptionContext)
    { }
    public void DataSourceGot(DbConnection connection, DbConnectionInterceptionContext<string> interceptionContext)
    { }
    public void Disposing(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
    { }
    public void Disposed(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
    { }
    public void EnlistingTransaction(DbConnection connection, EnlistTransactionInterceptionContext interceptionContext)
    { }
    public void EnlistedTransaction(DbConnection connection, EnlistTransactionInterceptionContext interceptionContext)
    { }
    public void Opening(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
    { }
    /// <summary>
    /// Called after <see cref="DbConnection.Open" /> or its async counterpart is invoked.
    /// </summary>
    /// <param name="connection">The connection that was opened.</param>
    /// <param name="interceptionContext">Contextual information associated with the call.</param>
    public void Opened(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
    {
        Check.NotNull(connection, "connection");
        Check.NotNull(interceptionContext, "interceptionContext");
        if (interceptionContext.Exception != null)
        {
            Write(
                interceptionContext.IsAsync
                    ? String.Format(StringResources.ConnectionOpenErrorLogAsync,
                        DateTimeOffset.UtcNow, interceptionContext.Exception.Message)
                    : String.Format(StringResources.ConnectionOpenErrorLog, DateTimeOffset.UtcNow, interceptionContext.Exception.Message));
        }
        else if (interceptionContext.TaskStatus.HasFlag(TaskStatus.Canceled))
        {
            Write(String.Format(StringResources.ConnectionOpenCanceledLog, DateTimeOffset.UtcNow));
        }
        else
        {
            Write(
                interceptionContext.IsAsync
                    ? String.Format(StringResources.ConnectionOpenedLogAsync, DateTimeOffset.UtcNow)
                    : String.Format(StringResources.ConnectionOpenedLog, DateTimeOffset.UtcNow));
        }
    }
    public void ServerVersionGetting(DbConnection connection, DbConnectionInterceptionContext<string> interceptionContext)
    { }
    public void ServerVersionGot(DbConnection connection, DbConnectionInterceptionContext<string> interceptionContext)
    { }
    public void StateGetting(DbConnection connection, DbConnectionInterceptionContext<ConnectionState> interceptionContext)
    { }
    public void StateGot(DbConnection connection, DbConnectionInterceptionContext<ConnectionState> interceptionContext)
    { } 
    #endregion
}

internal class Check
{
    public static T NotNull<T>(T value, string parameterName)
        where T : class
    {
        if (value == null)
        {
            throw new ArgumentNullException(parameterName);
        }
        return value;
    }
}

StringResources.resx:
CommandLogCanceled          Canceled in {0} ms{1}
CommandLogComplete          Completed in {0} ms with result: {1}
CommandLogFailed            Failed in {0} ms with error: {1}
ConnectionClosedLog         Closed connection at {0}
ConnectionCloseErrorLog     Failed to close connection at {0} with error: {1}
ConnectionOpenCanceledLog   Cancelled open connection at {0}
ConnectionOpenedLog         Opened connection at {0}
ConnectionOpenedLogAsync    Opened connection asynchronously at {0}
ConnectionOpenErrorLog      Failed to open connection at {0} with error: {1}
ConnectionOpenErrorLogAsync Failed to open connection asynchronously at {0} with error: {1}

相当简单,但你不能使用 System.Timers.Timer 对象吗?调用在 EF 代码之前开始,调用在 EF 代码之后结束。如果你有异步代码,你可以调用.结果并删除等待,以便同步运行代码并定时调用。除此之外,如果使用 SQL 探查器并从另一个值中减去一个值(计时器 - 探查器(,你将了解执行 EF 代码所需的时间。

此处的另一个解决方案是从某个更高级别(从 DAL 方法级别(测量查询。因此,您可以通过自定义属性标记每个db/dal方法,该属性将收集有关方法执行时间的信息并执行所需的操作(日志记录(。

例如,您可以使用 Fody lib 和 Fody 的 MethodTimer 扩展。

几句话如何:

例如,您有使用方法"GetAll"的存储库UsersRepositry

//semi-pseudo code
public class UsersRepository {
   ...
   public async Task<List<User>> GetAllAsync(){
      return await _efDbContext.Users.AllAsync();
   }   
}

使用Fody,您可以通过[Time]属性对其进行标记:

[Time]
public async Task<List<User>> GetAllAsync(){
      return await _efDbContext.Users.AllAsync();
}  

并添加静态记录器,日志执行时间:

public static class MethodTimeLogger
    {
        public static void Log(MethodBase methodBase, TimeSpan elapsed, string message)
        {
            //Do some logging here, for example : 
            YourLogger.Info($"{methodBase.DeclaringType.Name}.{methodBase.Name} : {elapsed.TotalMilliseconds}ms");
        }
    }

工作原理 :

Fody 将在 IL 级别的编译时间重写您标记的方法:

public class UsersRepository 
{
    public async Task<List<User>> GetAllAsync()
    {
        var stopwatch = Stopwatch.StartNew();
        try
        {
            //Some code u are curious how long it takes
            return await _efDbContext.Users.AllAsync();
        }
        finally
        {
            stopwatch.Stop();
            MethodTimeLogger.Log(methodof(GetAllAsync), stopwatch.Elapsed);
        }
    }
}

结论:测量 DAL 方法执行时间的好方法。如果不映射实体,则无法在此处记录某些内部 DbCommand 信息(例如 sql 查询(或原始执行时间,但通常取决于您的需求。就我而言,只需记录方法/执行时间就足够了。

希望,它会帮助某人。有一个好的编码!