Efcore: Implement events for before and after SaveChanges

Created on 3 Jun 2019  路  25Comments  路  Source: dotnet/efcore

Splitting this out as a separate issue as required on #626 so we can track it independently of other life-cycle hooks.

  • SavingChanges: An event fired when the entity object is just about to be written to the database.
  • SavedChanges: An event fired after the SavingChanges operation has completed (successfully or otherwise)
area-change-tracking closed-fixed punted-for-3.0 type-enhancement

Most helpful comment

@SARAVANA1501 It should more closely follow the pattern for other interceptors, including appropriate use of event data classes, sync/async overloads, interception results, etc. Off the top of my head something like this:

```C#
public class SaveChangesEventData : DbContextEventData
{
public SaveChangesEventData(
EventDefinitionBase eventDefinition,
Func messageGenerator,
DbContext context,
IList entriesToSave,
bool acceptChangesEnabled)
: base(eventDefinition, messageGenerator, context)
{
// TODO
}
}

public interface ISaveChangesInterceptor : IInterceptor
{
InterceptionResult SavingChanges(
SaveChangesEventData eventData,
InterceptionResult result);

Task<InterceptionResult<int>> SavingChangesAsync(
    SaveChangesEventData eventData,
    InterceptionResult<int> result,
    CancellationToken cancellationToken = default); 

int SavedChanges(
    SaveChangesCompletedEventData eventData,
    int result); 

Task<int> SavedChangesAsync(
    SaveChangesCompletedEventData eventData,
    int result,
    CancellationToken cancellationToken = default); 

void SavingChangesFiled(
    SaveChangesFailedEventData eventData);

}
```

All 25 comments

For my main use case: A query level cache, it looks like I'd need an event after SaveChanges.
Specifically, I need to know if the transaction has been committed (succeeded) or has been rolled back.

The query level cache we used for EF6 was based on this project: https://github.com/moozzyk/EFCache
We're basically thinking of porting this to EFCore if the necessary events are supported. Of course things maybe can be simplified.

Basically EFCache is using a custom DBCommand to figure out which entity sets are affected during Save/Rollback. It only needs to invalidate every cache entry of the specific entity set, so that's all the information it needs. The invalidation is done via IDbTransactionInterceptor's RolledBack and Committed hooks.

If the new AfterSaveChanges event of EFCore supported the following this logic described above could be simplified:

  • The event needs to expose if the transaction got committed or if it got rolled back.
  • If it could expose which entity sets are affected by the transaction, much of the logic of EFCache could be simplified.

See also this comment by the author of EFCache: https://github.com/aspnet/EntityFrameworkCore/issues/5858#issuecomment-306441806

@nphmuller Thanks for the info. I've also been doing some experiments with database interceptors, similar to EF6, which might be a better fit for this.

My use case for this would be logging and audit trails.

@ajcvickers Could we please have this for 3.1 (along with it's counterpart in #15911)

@SidShetye We haven't decided what will be in 3.1 yet.

@ajcvickers I would like to contribute this, it would be great if you provide more implementation details.

Removing to discuss in triage: would an interceptor or normal events be better here?

For us, the normal event is what is required.

@SARAVANA1501 We discussed this in triage and agreed that the interceptor is most appropriate. This will allow SaveChanges to have other functionality than is normal. See the patterns for existing interceptors (e.g. IDbCommandInterceptor) to get started.

Writing events to fire when the interceptor fires should be trivial. If not, we could revisit simple events as well.

@ajcvickers , Will take a look IDbCommandInterceptor

public interface IDbContextLifeCycleHook { void OnSaveChangesStarting(); void OnSaveChangesCompleted(int entitiesSaved); }

OnSaveChangesStarting invoked before save changes in DbContext.
OnSaveChangesCompleted invoked after save changes in DbContext.

@ajcvickers will it serve the purpose?

@SARAVANA1501 It should more closely follow the pattern for other interceptors, including appropriate use of event data classes, sync/async overloads, interception results, etc. Off the top of my head something like this:

```C#
public class SaveChangesEventData : DbContextEventData
{
public SaveChangesEventData(
EventDefinitionBase eventDefinition,
Func messageGenerator,
DbContext context,
IList entriesToSave,
bool acceptChangesEnabled)
: base(eventDefinition, messageGenerator, context)
{
// TODO
}
}

public interface ISaveChangesInterceptor : IInterceptor
{
InterceptionResult SavingChanges(
SaveChangesEventData eventData,
InterceptionResult result);

Task<InterceptionResult<int>> SavingChangesAsync(
    SaveChangesEventData eventData,
    InterceptionResult<int> result,
    CancellationToken cancellationToken = default); 

int SavedChanges(
    SaveChangesCompletedEventData eventData,
    int result); 

Task<int> SavedChangesAsync(
    SaveChangesCompletedEventData eventData,
    int result,
    CancellationToken cancellationToken = default); 

void SavingChangesFiled(
    SaveChangesFailedEventData eventData);

}
```

@ajcvickers Taking one step back, based off this,

would an interceptor or normal events be better here?

what's the difference between the interceptors and events? Just a class vs a function or something more conceptually different? We've historically registered on the events, not interceptors, so that's our familiar go-to.

The proposal above at face value seems like it may cover what we're looking for by wrapping our events into an interceptor class. (BTW SavingChangesFiled should be SavingChangesFailed right?).

How would one register the interceptor with EF? I guess it'll be very clear once this is implemented and there is a code sample demonstrating it's usage in documentation.

@SARAVANA1501 Thanks for picking this up. Do you think it'll make it to the EF Core 3.1 release?

@SidShetye

Thanks for picking this up. Do you think it'll make it to the EF Core 3.1 release?

Pretty sure it鈥檚 been mentioned that 3.1 will basically be only bug fixes.

Raised draft PR to get initial feedback, after i will implement test suite.

SaveChangesCompleted method in CoreLoggerExtension.cs is void.
is it right to place interceptor calling there?

@SidShetye Look at the existing interceptors. Registration would be the same for the new one.

This won't get done for 3.1. 3.1 is primarily about stabilizing after 3.0.

For those who came here via SEO: because I needed this functionality myself, I created my own package with lifecycle hooks.

You can download it via NuGet. Full docs are available on the Github page of the project.

InterceptionResult SavingChanges(
SaveChangesEventData eventData,
InterceptionResult result);

@ajcvickers Do we really need to return? In saving changes event we dont have any value to return.

@ajcvickers How to enable diagnostics source in CoreLoggerExtensions.cs method public static void SaveChangesStarting( [NotNull] this IDiagnosticsLogger<DbLoggerCategory.Update> diagnostics, [NotNull] DbContext context)?

@SARAVANA1501 Yes, for cancellation. Although shouldn't this handle the number of entities saved?

I don't understand your second question. Can you clarify what you mean?

@ajcvickers here At the time of calling SaveChanges Starting We don't have entities saved count.

@ajcvickers here How to enable diagnostics.DiagnosticSource for save changes for testing? Interceptor invoked inside this if.

@ajcvickers Did you get chance to look at the above code? I am waiting for your comments to continue with other test cases.

@SARAVANA1501 Sorry for being slow here. I think it might help if I provide a details on how an interceptor is intended to work:

  • Each conceptual event will have a "before" and an "after" interceptor method.
  • One purpose of the before method is to allow the interceptor to stop execution of this event--so in this case this would prevent EF from actually saving changes to the database.
  • When this happens EF still needs the result to continue execution. So in this case the number of entities saved needs to be returned from the interceptor as the InterceptionResult.

The best way to get this all right is to thoroughly understand some of the existing interceptors and repeat the pattern here. For example, IDbCommandInterceptor has some good examples which do similar things. All the testing should also follow the patterns already used for existing interceptors.

With no recent motion here would anyone object to me picking this up?

@SARAVANA1501?

Was this page helpful?
0 / 5 - 0 ratings