Skip to content

Commit

Permalink
Interceptor for DbUpdateConcurrencyException
Browse files Browse the repository at this point in the history
Part of #626
Part of #16260
Allows #10443 functionally, although not optimally

Relational version allows access to the connection, command, and reader from an async context, such that database commands could be used in the interceptor.
  • Loading branch information
ajcvickers committed Jun 23, 2022
1 parent 62fb246 commit 6b1fb61
Show file tree
Hide file tree
Showing 32 changed files with 828 additions and 131 deletions.
28 changes: 22 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,14 @@ 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, exception).IsSuppressed)
{
throw exception;
}
}
}

Expand Down Expand Up @@ -158,7 +165,15 @@ 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, exception, cancellationToken).ConfigureAwait(false)).IsSuppressed)
{
throw exception;
}
}
}

Expand Down Expand Up @@ -359,18 +374,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).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).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).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,
Exception 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; }
}
Loading

0 comments on commit 6b1fb61

Please sign in to comment.