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

Implement events for before and after SaveChanges #15910

Closed
ajcvickers opened this issue Jun 3, 2019 · 25 comments · Fixed by #21862
Closed

Implement events for before and after SaveChanges #15910

ajcvickers opened this issue Jun 3, 2019 · 25 comments · Fixed by #21862
Labels
area-change-tracking area-save-changes closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. punted-for-3.0 type-enhancement
Milestone

Comments

@ajcvickers
Copy link
Member

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)
@nphmuller
Copy link

nphmuller commented Jun 7, 2019

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: #5858 (comment)

@ajcvickers
Copy link
Member Author

@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.

@douglasg14b
Copy link

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

@SidShetye
Copy link

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

@ajcvickers
Copy link
Member Author

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

@ajcvickers ajcvickers added the good first issue This issue should be relatively straightforward to fix. label Sep 2, 2019
@SARAVANA1501
Copy link
Contributor

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

@ajcvickers
Copy link
Member Author

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

@ajcvickers ajcvickers removed this from the Backlog milestone Oct 15, 2019
@SidShetye
Copy link

For us, the normal event is what is required.

@ajcvickers
Copy link
Member Author

@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 ajcvickers added this to the Backlog milestone Oct 18, 2019
@SARAVANA1501
Copy link
Contributor

@ajcvickers , Will take a look IDbCommandInterceptor

@SARAVANA1501
Copy link
Contributor

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?

@ajcvickers
Copy link
Member Author

@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:

public class SaveChangesEventData : DbContextEventData
{
    public SaveChangesEventData(
        EventDefinitionBase eventDefinition, 
        Func<EventDefinitionBase, EventData, string> messageGenerator, 
        DbContext context,
        IList<IUpdateEntry> entriesToSave,
        bool acceptChangesEnabled) 
        : base(eventDefinition, messageGenerator, context)
    {
        // TODO
    }
}

public interface ISaveChangesInterceptor : IInterceptor
{
    InterceptionResult<int> SavingChanges(
        SaveChangesEventData eventData,
        InterceptionResult<int> 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); 
}

@SidShetye
Copy link

@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?

@nphmuller
Copy link

@SidShetye

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

Pretty sure it’s been mentioned that 3.1 will basically be only bug fixes.

@SARAVANA1501
Copy link
Contributor

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

@ajcvickers
Copy link
Member Author

@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.

@dncnkrs
Copy link

dncnkrs commented Jul 25, 2020

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

@SARAVANA1501?

ajcvickers added a commit that referenced this issue Jul 30, 2020
ajcvickers added a commit that referenced this issue Jul 30, 2020
ajcvickers added a commit that referenced this issue Jul 30, 2020
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.
ajcvickers added a commit that referenced this issue Jul 30, 2020
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.
@ajcvickers ajcvickers added closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. and removed good first issue This issue should be relatively straightforward to fix. labels Jul 30, 2020
ajcvickers added a commit that referenced this issue Jul 30, 2020
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.
@ajcvickers ajcvickers modified the milestones: 5.0.0, 5.0.0-rc1 Aug 14, 2020
@ajcvickers ajcvickers modified the milestones: 5.0.0-rc1, 5.0.0 Nov 7, 2020
@ajcvickers ajcvickers removed their assignment Sep 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-change-tracking area-save-changes closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. punted-for-3.0 type-enhancement
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants