Skip to content

Commit

Permalink
Add interception for DbDataReader.Close (#28290)
Browse files Browse the repository at this point in the history
Part of #626
Fixes #23535

Also added tests to:
 - Show that this can be used to get statistics from a query, as requested in #23535.
 - Show that Close and/or Dispose can be suppressed, as requested in #24295.
  • Loading branch information
ajcvickers committed Jun 23, 2022
1 parent df3ae11 commit 62fb246
Show file tree
Hide file tree
Showing 18 changed files with 837 additions and 80 deletions.
50 changes: 50 additions & 0 deletions src/EFCore.Relational/Diagnostics/DataReaderClosingEventData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.Diagnostics;

/// <summary>
/// <see cref="DiagnosticSource" /> event payload for <see cref="RelationalEventId.DataReaderClosing" />.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-diagnostics">Logging, events, and diagnostics</see> for more information and examples.
/// </remarks>
public class DataReaderClosingEventData : DataReaderEventData
{
/// <summary>
/// Constructs a <see cref="DiagnosticSource" /> event payload for <see cref="RelationalEventId.DataReaderClosing" />.
/// </summary>
/// <param name="eventDefinition">The event definition.</param>
/// <param name="messageGenerator">A delegate that generates a log message for this event.</param>
/// <param name="command">The <see cref="DbCommand" /> that created the reader.</param>
/// <param name="dataReader">The <see cref="DbDataReader" /> that is being disposed.</param>
/// <param name="context">The <see cref="DbContext" /> currently being used, to null if not known.</param>
/// <param name="commandId">A correlation ID that identifies the <see cref="DbCommand" /> instance being used.</param>
/// <param name="connectionId">A correlation ID that identifies the <see cref="DbConnection" /> instance being used.</param>
/// <param name="async">Indicates whether or not the command was executed asynchronously.</param>
/// <param name="recordsAffected">Gets the number of rows changed, inserted, or deleted by execution of the SQL statement.</param>
/// <param name="readCount">Gets the number of read operations performed by this reader.</param>
/// <param name="startTime">The time when the data reader was created.</param>
public DataReaderClosingEventData(
EventDefinitionBase eventDefinition,
Func<EventDefinitionBase, EventData, string> messageGenerator,
DbCommand command,
DbDataReader dataReader,
DbContext? context,
Guid commandId,
Guid connectionId,
bool async,
int recordsAffected,
int readCount,
DateTimeOffset startTime)
: base(
eventDefinition, messageGenerator, command, dataReader, context, commandId, connectionId, recordsAffected, readCount, startTime)
{
IsAsync = async;
}

/// <summary>
/// Indicates whether or not the operation is being executed asynchronously.
/// </summary>
public virtual bool IsAsync { get; }
}
57 changes: 10 additions & 47 deletions src/EFCore.Relational/Diagnostics/DataReaderDisposingEventData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Diagnostics;
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-diagnostics">Logging, events, and diagnostics</see> for more information and examples.
/// </remarks>
public class DataReaderDisposingEventData : DbContextEventData
public class DataReaderDisposingEventData : DataReaderEventData
{
/// <summary>
/// Constructs a <see cref="DiagnosticSource" /> event payload for <see cref="RelationalEventId.DataReaderDisposing" />.
Expand All @@ -23,8 +23,11 @@ public class DataReaderDisposingEventData : DbContextEventData
/// <param name="connectionId">A correlation ID that identifies the <see cref="DbConnection" /> instance being used.</param>
/// <param name="recordsAffected">Gets the number of rows changed, inserted, or deleted by execution of the SQL statement.</param>
/// <param name="readCount">Gets the number of read operations performed by this reader.</param>
/// <param name="startTime">The start time of this event.</param>
/// <param name="duration">The duration this event.</param>
/// <param name="startTime">The time when the data reader was created.</param>
/// <param name="duration">
/// The duration from the time the data reader is created until it is disposed. This corresponds to the time reading
/// for reading results of a query.
/// </param>
public DataReaderDisposingEventData(
EventDefinitionBase eventDefinition,
Func<EventDefinitionBase, EventData, string> messageGenerator,
Expand All @@ -37,55 +40,15 @@ public DataReaderDisposingEventData(
int readCount,
DateTimeOffset startTime,
TimeSpan duration)
: base(eventDefinition, messageGenerator, context)
: base(
eventDefinition, messageGenerator, command, dataReader, context, commandId, connectionId, recordsAffected, readCount, startTime)
{
Command = command;
DataReader = dataReader;
CommandId = commandId;
ConnectionId = connectionId;
RecordsAffected = recordsAffected;
ReadCount = readCount;
StartTime = startTime;
Duration = duration;
}

/// <summary>
/// The <see cref="DbCommand" /> that created the reader.
/// </summary>
public virtual DbCommand Command { get; }

/// <summary>
/// The <see cref="DbDataReader" /> that is being disposed.
/// </summary>
public virtual DbDataReader DataReader { get; }

/// <summary>
/// A correlation ID that identifies the <see cref="DbCommand" /> instance being used.
/// </summary>
public virtual Guid CommandId { get; }

/// <summary>
/// A correlation ID that identifies the <see cref="DbConnection" /> instance being used.
/// </summary>
public virtual Guid ConnectionId { get; }

/// <summary>
/// Gets the number of rows changed, inserted, or deleted by execution of the SQL statement.
/// </summary>
public virtual int RecordsAffected { get; }

/// <summary>
/// Gets the number of read operations performed by this reader.
/// </summary>
public virtual int ReadCount { get; }

/// <summary>
/// The start time of this event.
/// </summary>
public virtual DateTimeOffset StartTime { get; }

/// <summary>
/// The duration this event.
/// The duration from the time the data reader is created until it is disposed. This corresponds to the time reading
/// for reading results of a query.
/// </summary>
public virtual TimeSpan Duration { get; }
}
83 changes: 83 additions & 0 deletions src/EFCore.Relational/Diagnostics/DataReaderEventData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.Diagnostics;

/// <summary>
/// <see cref="DiagnosticSource" /> event payload for <see cref="RelationalEventId.DataReaderClosing" />.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-diagnostics">Logging, events, and diagnostics</see> for more information and examples.
/// </remarks>
public class DataReaderEventData : DbContextEventData
{
/// <summary>
/// Constructs a <see cref="DiagnosticSource" /> event payload for <see cref="RelationalEventId.DataReaderClosing" />.
/// </summary>
/// <param name="eventDefinition">The event definition.</param>
/// <param name="messageGenerator">A delegate that generates a log message for this event.</param>
/// <param name="command">The <see cref="DbCommand" /> that created the reader.</param>
/// <param name="dataReader">The <see cref="DbDataReader" /> that is being disposed.</param>
/// <param name="context">The <see cref="DbContext" /> currently being used, to null if not known.</param>
/// <param name="commandId">A correlation ID that identifies the <see cref="DbCommand" /> instance being used.</param>
/// <param name="connectionId">A correlation ID that identifies the <see cref="DbConnection" /> instance being used.</param>
/// <param name="recordsAffected">Gets the number of rows changed, inserted, or deleted by execution of the SQL statement.</param>
/// <param name="readCount">Gets the number of read operations performed by this reader.</param>
/// <param name="startTime">The start time of this event.</param>
public DataReaderEventData(
EventDefinitionBase eventDefinition,
Func<EventDefinitionBase, EventData, string> messageGenerator,
DbCommand command,
DbDataReader dataReader,
DbContext? context,
Guid commandId,
Guid connectionId,
int recordsAffected,
int readCount,
DateTimeOffset startTime)
: base(eventDefinition, messageGenerator, context)
{
Command = command;
DataReader = dataReader;
CommandId = commandId;
ConnectionId = connectionId;
RecordsAffected = recordsAffected;
ReadCount = readCount;
StartTime = startTime;
}

/// <summary>
/// The <see cref="DbCommand" /> that created the reader.
/// </summary>
public virtual DbCommand Command { get; }

/// <summary>
/// The <see cref="DbDataReader" /> that is being disposed.
/// </summary>
public virtual DbDataReader DataReader { get; }

/// <summary>
/// A correlation ID that identifies the <see cref="DbCommand" /> instance being used.
/// </summary>
public virtual Guid CommandId { get; }

/// <summary>
/// A correlation ID that identifies the <see cref="DbConnection" /> instance being used.
/// </summary>
public virtual Guid ConnectionId { get; }

/// <summary>
/// Gets the number of rows changed, inserted, or deleted by execution of the SQL statement.
/// </summary>
public virtual int RecordsAffected { get; }

/// <summary>
/// Gets the number of read operations performed by this reader.
/// </summary>
public virtual int ReadCount { get; }

/// <summary>
/// The start time of this event.
/// </summary>
public virtual DateTimeOffset StartTime { get; }
}
42 changes: 42 additions & 0 deletions src/EFCore.Relational/Diagnostics/IDbCommandInterceptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,48 @@ void CommandFailed(DbCommand command, CommandErrorEventData eventData)
Task CommandFailedAsync(DbCommand command, CommandErrorEventData eventData, CancellationToken cancellationToken = default)
=> Task.CompletedTask;

/// <summary>
/// Called just before EF intends to call <see cref="DbDataReader.Close()" />.
/// </summary>
/// <param name="command">The command.</param>
/// <param name="eventData">Contextual information about the command.</param>
/// <param name="result">
/// Represents the current result if one exists.
/// This value will have <see cref="InterceptionResult.IsSuppressed" /> set to <see langword="true" /> if some previous
/// interceptor suppressed execution by calling <see cref="InterceptionResult.Suppress" />.
/// This value is typically used as the return value for the implementation of this method.
/// </param>
/// <returns>
/// If <see cref="InterceptionResult.IsSuppressed" /> is <see langword="false" />, then EF will continue as normal.
/// If <see cref="InterceptionResult.IsSuppressed" /> is <see langword="true" />, then EF will suppress the operation
/// it was about to perform.
/// An implementation of this method for any interceptor that is not attempting to suppress
/// the operation is to return the <paramref name="result" /> value passed in.
/// </returns>
InterceptionResult DataReaderClosing(DbCommand command, DataReaderClosingEventData eventData, InterceptionResult result)
=> result;

/// <summary>
/// Called just before EF intends to call <see cref="DbDataReader.CloseAsync()" /> in an async context.
/// </summary>
/// <param name="command">The command.</param>
/// <param name="eventData">Contextual information about the command.</param>
/// <param name="result">
/// Represents the current result if one exists.
/// This value will have <see cref="InterceptionResult.IsSuppressed" /> set to <see langword="true" /> if some previous
/// interceptor suppressed execution by calling <see cref="InterceptionResult.Suppress" />.
/// This value is typically used as the return value for the implementation of this method.
/// </param>
/// <returns>
/// If <see cref="InterceptionResult.IsSuppressed" /> is <see langword="false" />, then EF will continue as normal.
/// If <see cref="InterceptionResult.IsSuppressed" /> is <see langword="true" />, then EF will suppress the operation
/// it was about to perform.
/// An implementation of this method for any interceptor that is not attempting to suppress
/// the operation is to return the <paramref name="result" /> value passed in.
/// </returns>
ValueTask<InterceptionResult> DataReaderClosingAsync(DbCommand command, DataReaderClosingEventData eventData, InterceptionResult result)
=> new(result);

/// <summary>
/// Called when execution of a <see cref="DbDataReader" /> is about to be disposed.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,46 @@ InterceptionResult DataReaderDisposing(
DateTimeOffset startTime,
TimeSpan duration);

/// <summary>
/// Logs for the <see cref="RelationalEventId.DataReaderClosing" /> event.
/// </summary>
/// <param name="connection">The connection.</param>
/// <param name="command">The database command object.</param>
/// <param name="dataReader">The data reader.</param>
/// <param name="commandId">The correlation ID associated with the given <see cref="DbCommand" />.</param>
/// <param name="recordsAffected">The number of records in the database that were affected.</param>
/// <param name="readCount">The number of records that were read.</param>
/// <param name="startTime">The time that the operation was started.</param>
/// <returns>The result of execution, which may have been modified by an interceptor.</returns>
InterceptionResult DataReaderClosing(
IRelationalConnection connection,
DbCommand command,
DbDataReader dataReader,
Guid commandId,
int recordsAffected,
int readCount,
DateTimeOffset startTime);

/// <summary>
/// Logs for the <see cref="RelationalEventId.DataReaderClosing" /> event.
/// </summary>
/// <param name="connection">The connection.</param>
/// <param name="command">The database command object.</param>
/// <param name="dataReader">The data reader.</param>
/// <param name="commandId">The correlation ID associated with the given <see cref="DbCommand" />.</param>
/// <param name="recordsAffected">The number of records in the database that were affected.</param>
/// <param name="readCount">The number of records that were read.</param>
/// <param name="startTime">The time that the operation was started.</param>
/// <returns>The result of execution, which may have been modified by an interceptor.</returns>
ValueTask<InterceptionResult> DataReaderClosingAsync(
IRelationalConnection connection,
DbCommand command,
DbDataReader dataReader,
Guid commandId,
int recordsAffected,
int readCount,
DateTimeOffset startTime);

/// <summary>
/// Whether <see cref="RelationalEventId.CommandCreating" /> or <see cref="RelationalEventId.CommandCreated" /> need
/// to be logged.
Expand All @@ -508,11 +548,13 @@ InterceptionResult DataReaderDisposing(
/// </summary>
bool ShouldLogCommandExecute(DateTimeOffset now);

/// <summary>
/// Whether <see cref="RelationalEventId.DataReaderClosing" /> needs to be logged.
/// </summary>
bool ShouldLogDataReaderClose(DateTimeOffset now);

/// <summary>
/// Whether <see cref="RelationalEventId.DataReaderDisposing" /> needs to be logged.
/// </summary>
bool ShouldLogDataReaderDispose(DateTimeOffset now);

private bool ShouldLogParameterValues(DbCommand command)
=> command.Parameters.Count > 0 && ShouldLogSensitiveData();
}
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,30 @@ await _interceptors[i].CommandFailedAsync(command, eventData, cancellationToken)
}
}

public InterceptionResult DataReaderClosing(DbCommand command, DataReaderClosingEventData eventData, InterceptionResult result)
{
for (var i = 0; i < _interceptors.Length; i++)
{
result = _interceptors[i].DataReaderClosing(command, eventData, result);
}

return result;
}

public async ValueTask<InterceptionResult> DataReaderClosingAsync(
DbCommand command,
DataReaderClosingEventData eventData,
InterceptionResult result)
{
for (var i = 0; i < _interceptors.Length; i++)
{
result = await _interceptors[i].DataReaderClosingAsync(command, eventData, result)
.ConfigureAwait(false);
}

return result;
}

public InterceptionResult DataReaderDisposing(
DbCommand command,
DataReaderDisposingEventData eventData,
Expand Down
Loading

0 comments on commit 62fb246

Please sign in to comment.