Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interceptor for DbUpdateConcurrencyException #28305

Merged
merged 2 commits into from
Jun 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,15 @@ public override int SaveChanges(IList<IUpdateEntry> entries)
}
catch (Exception ex) when (ex is not DbUpdateException and not OperationCanceledException)
{
throw WrapUpdateException(ex, entry);
var errorEntries = new[] { entry };
var exception = WrapUpdateException(ex, errorEntries);

if (exception is not DbUpdateConcurrencyException
|| !Dependencies.Logger.OptimisticConcurrencyException(
entry.Context, errorEntries, (DbUpdateConcurrencyException)exception, null).IsSuppressed)
{
throw exception;
}
}
}

Expand Down Expand Up @@ -158,7 +166,16 @@ public override async Task<int> SaveChangesAsync(
}
catch (Exception ex) when (ex is not DbUpdateException and not OperationCanceledException)
{
throw WrapUpdateException(ex, entry);
var errorEntries = new[] { entry };
var exception = WrapUpdateException(ex, errorEntries);

if (exception is not DbUpdateConcurrencyException
|| !(await Dependencies.Logger.OptimisticConcurrencyExceptionAsync(
entry.Context, errorEntries, (DbUpdateConcurrencyException)exception, null, cancellationToken)
.ConfigureAwait(false)).IsSuppressed)
{
throw exception;
}
}
}

Expand Down Expand Up @@ -359,18 +376,19 @@ private IUpdateEntry GetRootDocument(InternalEntityEntry entry)
}
#pragma warning restore EF1001 // Internal EF Core API usage.

private Exception WrapUpdateException(Exception exception, IUpdateEntry entry)
private Exception WrapUpdateException(Exception exception, IReadOnlyList<IUpdateEntry> entries)
{
var entry = entries[0];
var documentSource = GetDocumentSource(entry.EntityType);
var id = documentSource.GetId(entry.SharedIdentityEntry ?? entry);

return exception switch
{
CosmosException { StatusCode: HttpStatusCode.PreconditionFailed }
=> new DbUpdateConcurrencyException(CosmosStrings.UpdateConflict(id), exception, new[] { entry }),
=> new DbUpdateConcurrencyException(CosmosStrings.UpdateConflict(id), exception, entries),
CosmosException { StatusCode: HttpStatusCode.Conflict }
=> new DbUpdateException(CosmosStrings.UpdateConflict(id), exception, new[] { entry }),
_ => new DbUpdateException(CosmosStrings.UpdateStoreException(id), exception, new[] { entry })
=> new DbUpdateException(CosmosStrings.UpdateConflict(id), exception, entries),
_ => new DbUpdateException(CosmosStrings.UpdateStoreException(id), exception, entries)
};
}
}
6 changes: 3 additions & 3 deletions src/EFCore.InMemory/Storage/Internal/IInMemoryTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,23 @@ public interface IInMemoryTable
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
void Create(IUpdateEntry entry);
void Create(IUpdateEntry entry, IDiagnosticsLogger<DbLoggerCategory.Update> updateLogger);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
void Delete(IUpdateEntry entry);
void Delete(IUpdateEntry entry, IDiagnosticsLogger<DbLoggerCategory.Update> updateLogger);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
void Update(IUpdateEntry entry);
void Update(IUpdateEntry entry, IDiagnosticsLogger<DbLoggerCategory.Update> updateLogger);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
8 changes: 4 additions & 4 deletions src/EFCore.InMemory/Storage/Internal/InMemoryStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,19 +176,19 @@ public virtual int ExecuteTransaction(
continue;
}

table.Delete(entry);
table.Delete(entry, updateLogger);
}

switch (entry.EntityState)
{
case EntityState.Added:
table.Create(entry);
table.Create(entry, updateLogger);
break;
case EntityState.Deleted:
table.Delete(entry);
table.Delete(entry, updateLogger);
break;
case EntityState.Modified:
table.Update(entry);
table.Update(entry, updateLogger);
break;
}

Expand Down
78 changes: 48 additions & 30 deletions src/EFCore.InMemory/Storage/Internal/InMemoryTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ private static List<ValueComparer> GetKeyComparers(IEnumerable<IProperty> proper
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual void Create(IUpdateEntry entry)
public virtual void Create(IUpdateEntry entry, IDiagnosticsLogger<DbLoggerCategory.Update> updateLogger)
{
var properties = entry.EntityType.GetProperties().ToList();
var row = new object?[properties.Count];
Expand Down Expand Up @@ -199,7 +199,7 @@ public virtual void Create(IUpdateEntry entry)
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual void Delete(IUpdateEntry entry)
public virtual void Delete(IUpdateEntry entry, IDiagnosticsLogger<DbLoggerCategory.Update> updateLogger)
{
var key = CreateKey(entry);

Expand All @@ -215,14 +215,19 @@ public virtual void Delete(IUpdateEntry entry)

if (concurrencyConflicts.Count > 0)
{
ThrowUpdateConcurrencyException(entry, concurrencyConflicts);
ThrowUpdateConcurrencyException(entry, concurrencyConflicts, updateLogger);
}

_rows.Remove(key);
}
else
{
throw new DbUpdateConcurrencyException(InMemoryStrings.UpdateConcurrencyException, new[] { entry });
var entries = new[] { entry };
var exception = new DbUpdateConcurrencyException(InMemoryStrings.UpdateConcurrencyException, entries);
if (!updateLogger.OptimisticConcurrencyException(entry.Context, entries, exception, null).IsSuppressed)
{
throw exception;
}
}
}

Expand Down Expand Up @@ -263,7 +268,7 @@ private static bool IsConcurrencyConflict(
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual void Update(IUpdateEntry entry)
public virtual void Update(IUpdateEntry entry, IDiagnosticsLogger<DbLoggerCategory.Update> updateLogger)
{
var key = CreateKey(entry);

Expand Down Expand Up @@ -294,7 +299,7 @@ public virtual void Update(IUpdateEntry entry)

if (concurrencyConflicts.Count > 0)
{
ThrowUpdateConcurrencyException(entry, concurrencyConflicts);
ThrowUpdateConcurrencyException(entry, concurrencyConflicts, updateLogger);
}

if (nullabilityErrors.Count > 0)
Expand All @@ -308,7 +313,12 @@ public virtual void Update(IUpdateEntry entry)
}
else
{
throw new DbUpdateConcurrencyException(InMemoryStrings.UpdateConcurrencyException, new[] { entry });
var entries = new[] { entry };
var exception = new DbUpdateConcurrencyException(InMemoryStrings.UpdateConcurrencyException, entries);
if (!updateLogger.OptimisticConcurrencyException(entry.Context, entries, exception, null).IsSuppressed)
{
throw exception;
}
}
}

Expand Down Expand Up @@ -394,34 +404,42 @@ private void ThrowNullabilityErrorException(
}

/// <summary>
/// Throws an exception indicating that concurrency conflicts were detected.
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
/// <param name="entry">The update entry which resulted in the conflict(s).</param>
/// <param name="concurrencyConflicts">The conflicting properties with their associated database values.</param>
protected virtual void ThrowUpdateConcurrencyException(
IUpdateEntry entry,
Dictionary<IProperty, object?> concurrencyConflicts)
Dictionary<IProperty, object?> concurrencyConflicts,
IDiagnosticsLogger<DbLoggerCategory.Update> updateLogger)
{
if (_sensitiveLoggingEnabled)
var entries = new[] { entry };

var exception =
_sensitiveLoggingEnabled
? new DbUpdateConcurrencyException(
InMemoryStrings.UpdateConcurrencyTokenExceptionSensitive(
entry.EntityType.DisplayName(),
entry.BuildCurrentValuesString(entry.EntityType.FindPrimaryKey()!.Properties),
entry.BuildOriginalValuesString(concurrencyConflicts.Keys),
"{"
+ string.Join(
", ",
concurrencyConflicts.Select(
c => c.Key.Name + ": " + Convert.ToString(c.Value, CultureInfo.InvariantCulture)))
+ "}"),
entries)
: new DbUpdateConcurrencyException(
InMemoryStrings.UpdateConcurrencyTokenException(
entry.EntityType.DisplayName(),
concurrencyConflicts.Keys.Format()),
entries);


if (!updateLogger.OptimisticConcurrencyException(entry.Context, entries, exception, null).IsSuppressed)
{
throw new DbUpdateConcurrencyException(
InMemoryStrings.UpdateConcurrencyTokenExceptionSensitive(
entry.EntityType.DisplayName(),
entry.BuildCurrentValuesString(entry.EntityType.FindPrimaryKey()!.Properties),
entry.BuildOriginalValuesString(concurrencyConflicts.Keys),
"{"
+ string.Join(
", ",
concurrencyConflicts.Select(
c => c.Key.Name + ": " + Convert.ToString(c.Value, CultureInfo.InvariantCulture)))
+ "}"),
new[] { entry });
throw exception;
}

throw new DbUpdateConcurrencyException(
InMemoryStrings.UpdateConcurrencyTokenException(
entry.EntityType.DisplayName(),
concurrencyConflicts.Keys.Format()),
new[] { entry });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// 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>
/// A <see cref="DiagnosticSource" /> event payload used when a <see cref="DbUpdateConcurrencyException" /> is being thrown
/// from a relational database provider.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-diagnostics">Logging, events, and diagnostics</see> for more information and examples.
/// </remarks>
public class RelationalConcurrencyExceptionEventData : ConcurrencyExceptionEventData
{
/// <summary>
/// Constructs the event payload.
/// </summary>
/// <param name="eventDefinition">The event definition.</param>
/// <param name="messageGenerator">A delegate that generates a log message for this event.</param>
/// <param name="context">The current <see cref="DbContext" />.</param>
/// <param name="connection">The <see cref="DbConnection" /> being used.</param>
/// <param name="command">The <see cref="DbCommand" /> being used.</param>
/// <param name="dataReader">The <see cref="DbDataReader" /> being used.</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="entries">The entries that were involved in the concurrency violation.</param>
/// <param name="exception">The exception that will be thrown, unless throwing is suppressed.</param>
public RelationalConcurrencyExceptionEventData(
EventDefinitionBase eventDefinition,
Func<EventDefinitionBase, EventData, string> messageGenerator,
DbContext context,
DbConnection connection,
DbCommand command,
DbDataReader dataReader,
Guid commandId,
Guid connectionId,
IReadOnlyList<IUpdateEntry> entries,
DbUpdateConcurrencyException exception)
: base(eventDefinition, messageGenerator, context, entries, exception)
{
Connection = connection;
Command = command;
DataReader = dataReader;
CommandId = commandId;
ConnectionId = connectionId;
}

/// <summary>
/// The <see cref="DbConnection" /> being used.
/// </summary>
public virtual DbConnection Connection { get; }

/// <summary>
/// The <see cref="DbCommand" /> being used.
/// </summary>
public virtual DbCommand Command { get; }

/// <summary>
/// The <see cref="DbDataReader" /> being used.
/// </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; }
}
12 changes: 12 additions & 0 deletions src/EFCore.Relational/Storage/RelationalDataReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ public virtual void Initialize(
_stopwatch.Restart();
}

/// <summary>
/// Gets the underlying relational connection being used.
/// </summary>
public virtual IRelationalConnection RelationalConnection
=> _relationalConnection;

/// <summary>
/// Gets the underlying reader for the result set.
/// </summary>
Expand All @@ -70,6 +76,12 @@ public virtual DbDataReader DbDataReader
public virtual DbCommand DbCommand
=> _command;

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

/// <summary>
/// Calls <see cref="System.Data.Common.DbDataReader.Read" /> on the underlying <see cref="System.Data.Common.DbDataReader" />.
/// </summary>
Expand Down
Loading