Skip to content

Commit

Permalink
Add SaveChanges events
Browse files Browse the repository at this point in the history
Fixes #15910

I realized that the interceptor, while powerful, is hard to attach to without changing the context configuration. Therefore, this PR adds simple .NET events that can easily be attached to from outside the code that defines the context.

Also, fix to call the SaveChanges failed interceptor when the failure is an optimistic concurrency exception.
  • Loading branch information
ajcvickers committed Jul 30, 2020
1 parent f7fddae commit 1c42f7b
Show file tree
Hide file tree
Showing 9 changed files with 356 additions and 30 deletions.
49 changes: 46 additions & 3 deletions src/EFCore/DbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,8 @@ public virtual int SaveChanges(bool acceptAllChangesOnSuccess)
{
CheckDisposed();

SavingChanges?.Invoke(this, new SavingChangesEventArgs(acceptAllChangesOnSuccess));

var interceptionResult = DbContextDependencies.UpdateLogger.SaveChangesStarting(this);

TryDetectChanges();
Expand All @@ -496,18 +498,26 @@ public virtual int SaveChanges(bool acceptAllChangesOnSuccess)
? interceptionResult.Result
: DbContextDependencies.StateManager.SaveChanges(acceptAllChangesOnSuccess);

return DbContextDependencies.UpdateLogger.SaveChangesCompleted(this, entitiesSaved);
var result = DbContextDependencies.UpdateLogger.SaveChangesCompleted(this, entitiesSaved);

SavedChanges?.Invoke(this, new SavedChangesEventArgs(acceptAllChangesOnSuccess, result));

return result;
}
catch (DbUpdateConcurrencyException exception)
{
DbContextDependencies.UpdateLogger.OptimisticConcurrencyException(this, exception);

SaveChangesFailed?.Invoke(this, new SaveChangesFailedEventArgs(acceptAllChangesOnSuccess, exception));

throw;
}
catch (Exception exception)
{
DbContextDependencies.UpdateLogger.SaveChangesFailed(this, exception);

SaveChangesFailed?.Invoke(this, new SaveChangesFailedEventArgs(acceptAllChangesOnSuccess, exception));

throw;
}
}
Expand Down Expand Up @@ -595,6 +605,8 @@ public virtual async Task<int> SaveChangesAsync(
{
CheckDisposed();

SavingChanges?.Invoke(this, new SavingChangesEventArgs(acceptAllChangesOnSuccess));

var interceptionResult = await DbContextDependencies.UpdateLogger
.SaveChangesStartingAsync(this, cancellationToken).ConfigureAwait(false);

Expand All @@ -608,24 +620,51 @@ public virtual async Task<int> SaveChangesAsync(
.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken)
.ConfigureAwait(false);

return await DbContextDependencies.UpdateLogger
var result = await DbContextDependencies.UpdateLogger
.SaveChangesCompletedAsync(this, entitiesSaved, cancellationToken)
.ConfigureAwait(false);

SavedChanges?.Invoke(this, new SavedChangesEventArgs(acceptAllChangesOnSuccess, result));

return result;
}
catch (DbUpdateConcurrencyException exception)
{
DbContextDependencies.UpdateLogger.OptimisticConcurrencyException(this, exception);
await DbContextDependencies.UpdateLogger.OptimisticConcurrencyExceptionAsync(this, exception, cancellationToken)
.ConfigureAwait(false);

SaveChangesFailed?.Invoke(this, new SaveChangesFailedEventArgs(acceptAllChangesOnSuccess, exception));

throw;
}
catch (Exception exception)
{
await DbContextDependencies.UpdateLogger.SaveChangesFailedAsync(this, exception, cancellationToken).ConfigureAwait(false);

SaveChangesFailed?.Invoke(this, new SaveChangesFailedEventArgs(acceptAllChangesOnSuccess, exception));

throw;
}
}

/// <summary>
/// An event fired when an entity is tracked by the context, either because it was returned
/// from a tracking query, or because it was attached or added to the context.
/// </summary>
public event EventHandler<SavingChangesEventArgs> SavingChanges;

/// <summary>
/// An event fired when an entity is tracked by the context, either because it was returned
/// from a tracking query, or because it was attached or added to the context.
/// </summary>
public event EventHandler<SavedChangesEventArgs> SavedChanges;

/// <summary>
/// An event fired when an entity is tracked by the context, either because it was returned
/// from a tracking query, or because it was attached or added to the context.
/// </summary>
public event EventHandler<SaveChangesFailedEventArgs> SaveChangesFailed;

/// <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
Expand Down Expand Up @@ -705,6 +744,10 @@ void IResettableService.ResetState()
service.ResetState();
}

SavingChanges = null;
SavedChanges = null;
SaveChangesFailed = null;

_disposed = true;
}

Expand Down
62 changes: 52 additions & 10 deletions src/EFCore/Diagnostics/CoreLoggerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,7 @@ public static void SaveChangesFailed(

diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);

if (interceptor != null)
{
interceptor.SaveChangesFailed(eventData);
}
interceptor?.SaveChangesFailed(eventData);
}
}

Expand Down Expand Up @@ -140,18 +137,63 @@ public static void OptimisticConcurrencyException(
definition.Log(diagnostics, exception);
}

if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled))
if (diagnostics.NeedsEventData<ISaveChangesInterceptor>(definition,
out var interceptor, out var diagnosticSourceEnabled, out var simpleLogEnabled))
{
var eventData = new DbContextErrorEventData(
definition,
OptimisticConcurrencyException,
context,
exception);
var eventData = CreateDbContextErrorEventData(context, exception, definition);

diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);

interceptor?.SaveChangesFailed(eventData);
}
}

/// <summary>
/// Logs for the <see cref="CoreEventId.OptimisticConcurrencyException" /> event.
/// </summary>
/// <param name="diagnostics"> The diagnostics logger to use. </param>
/// <param name="context"> The context in use. </param>
/// <param name="exception"> The exception that caused this event. </param>
/// <param name="cancellationToken"> The cancellation token. </param>
/// <returns> A <see cref="ValueTask"/> for the async result. </returns>
public static ValueTask OptimisticConcurrencyExceptionAsync(
[NotNull] this IDiagnosticsLogger<DbLoggerCategory.Update> diagnostics,
[NotNull] DbContext context,
[NotNull] Exception exception,
CancellationToken cancellationToken = default)
{
var definition = CoreResources.LogOptimisticConcurrencyException(diagnostics);

if (diagnostics.ShouldLog(definition))
{
definition.Log(diagnostics, exception);
}

if (diagnostics.NeedsEventData<ISaveChangesInterceptor>(
definition,
out var interceptor, out var diagnosticSourceEnabled, out var simpleLogEnabled))
{
var eventData = CreateDbContextErrorEventData(context, exception, definition);

diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);

if (interceptor != null)
{
return interceptor.SaveChangesFailedAsync(eventData, cancellationToken);
}
}

return new ValueTask();
}

private static DbContextErrorEventData CreateDbContextErrorEventData(
DbContext context, Exception exception, EventDefinition<Exception> definition)
=> new DbContextErrorEventData(
definition,
OptimisticConcurrencyException,
context,
exception);

private static string OptimisticConcurrencyException(EventDefinitionBase definition, EventData payload)
{
var d = (EventDefinition<Exception>)definition;
Expand Down
28 changes: 28 additions & 0 deletions src/EFCore/SaveChangesEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;

namespace Microsoft.EntityFrameworkCore
{
/// <summary>
/// Base event arguments for the <see cref="M:DbContext.SaveChanges" /> and <see cref="M:DbContext.SaveChangesAsync" /> events.
/// </summary>
public abstract class SaveChangesEventArgs : EventArgs
{
/// <summary>
/// Creates a base event arguments instance for <see cref="M:DbContext.SaveChanges" />
/// or <see cref="M:DbContext.SaveChangesAsync" /> events.
/// </summary>
/// <param name="acceptAllChangesOnSuccess"> The value passed to SaveChanges. </param>
protected SaveChangesEventArgs(bool acceptAllChangesOnSuccess)
{
AcceptAllChangesOnSuccess = acceptAllChangesOnSuccess;
}

/// <summary>
/// The value passed to <see cref="M:DbContext.SaveChanges" /> or <see cref="M:DbContext.SaveChangesAsync" />.
/// </summary>
public virtual bool AcceptAllChangesOnSuccess { get; }
}
}
30 changes: 30 additions & 0 deletions src/EFCore/SaveChangesFailedEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using JetBrains.Annotations;

namespace Microsoft.EntityFrameworkCore
{
/// <summary>
/// Event arguments for the <see cref="DbContext.SaveChangesFailed" /> event.
/// </summary>
public class SaveChangesFailedEventArgs : SaveChangesEventArgs
{
/// <summary>
/// Creates a new <see cref="SaveChangesFailedEventArgs"/> instance with the exception that was thrown.
/// </summary>
/// <param name="acceptAllChangesOnSuccess"> The value passed to SaveChanges. </param>
/// <param name="exception"> The exception thrown. </param>
public SaveChangesFailedEventArgs(bool acceptAllChangesOnSuccess, [NotNull] Exception exception)
: base(acceptAllChangesOnSuccess)
{
Exception = exception;
}

/// <summary>
/// The exception thrown during<see cref="M:DbContext.SaveChanges"/> or <see cref="M:DbContext.SaveChangesAsync"/>.
/// </summary>
public virtual Exception Exception { get; }
}
}
27 changes: 27 additions & 0 deletions src/EFCore/SavedChangesEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace Microsoft.EntityFrameworkCore
{
/// <summary>
/// Event arguments for the <see cref="DbContext.SavedChanges" /> event.
/// </summary>
public class SavedChangesEventArgs : SaveChangesEventArgs
{
/// <summary>
/// Creates a new <see cref="SavedChangesEventArgs" /> instance with the given number of entities saved.
/// </summary>
/// <param name="acceptAllChangesOnSuccess"> The value passed to SaveChanges. </param>
/// <param name="entitiesSavedCount"> The number of entities saved. </param>
public SavedChangesEventArgs(bool acceptAllChangesOnSuccess, int entitiesSavedCount)
: base(acceptAllChangesOnSuccess)
{
EntitiesSavedCount = entitiesSavedCount;
}

/// <summary>
/// The number of entities saved.
/// </summary>
public virtual int EntitiesSavedCount { get; }
}
}
20 changes: 20 additions & 0 deletions src/EFCore/SavingChangesEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace Microsoft.EntityFrameworkCore
{
/// <summary>
/// Event arguments for the <see cref="DbContext.SavingChanges" /> event.
/// </summary>
public class SavingChangesEventArgs : SaveChangesEventArgs
{
/// <summary>
/// Creates event arguments for the <see cref="M:DbContext.SavingChanges" /> event.
/// </summary>
/// <param name="acceptAllChangesOnSuccess"> The value passed to SaveChanges. </param>
public SavingChangesEventArgs(bool acceptAllChangesOnSuccess)
: base(acceptAllChangesOnSuccess)
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.EntityFrameworkCore.TestUtilities;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Xunit.Sdk;

namespace Microsoft.EntityFrameworkCore
{
Expand All @@ -16,6 +17,8 @@ protected SaveChangesInterceptionInMemoryTestBase(InterceptionInMemoryFixtureBas
{
}

protected override bool SupportsOptimisticConcurrency => false;

public abstract class InterceptionInMemoryFixtureBase : InterceptionFixtureBase
{
protected override string StoreName => "SaveChangesInterception";
Expand Down
Loading

0 comments on commit 1c42f7b

Please sign in to comment.