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

Add affected entries to "SavedChanges" callback #26564

Open
Tracked by #22959
douglasg14b opened this issue Nov 7, 2021 · 9 comments
Open
Tracked by #22959

Add affected entries to "SavedChanges" callback #26564

douglasg14b opened this issue Nov 7, 2021 · 9 comments

Comments

@douglasg14b
Copy link

douglasg14b commented Nov 7, 2021

Ask a question

I need to perform an action after modified entities are persisted, but am having a hard time figuring out how to determine if an entity was modified after the fact? It doesn't seem like the SavedChanges callback includes a list of modified/new/deleted entities.

The specific use case is that I need to emit some domain events after changes have been persisted, but I don't know how to determine which entities where modified in the SavedChanges callback.

This seems like something that falls under: #626 but I wanted to check if there was an already supported method of doing this.

Include provider and version information

EF Core version: 5
Target framework: NET 5.0

@douglasg14b
Copy link
Author

douglasg14b commented Nov 7, 2021

I could override SaveChangesAsync(), manually keep track of which entities where modified, and perform this action after calling base.SaveChangesAsync(), though I get the feeling this is undesirable (?), if not then that a perfectly workable route.

@ajcvickers
Copy link
Contributor

ajcvickers commented Nov 8, 2021

@douglasg14b Call SaveChanges with acceptAllChangesOnSuccess set to false, and then manually call AcceptAllChanges. For example:

context.SaveChanges(acceptAllChangesOnSuccess: false);
context.ChangeTracker.AcceptAllChanges();

This will cause the event to fire before all the entity states have been changed.

Note for triage: it would be useful to have events for accepting changes to cover this.

@douglasg14b
Copy link
Author

Gotcha, that makes sense then. Thanks!

@ajcvickers
Copy link
Contributor

Related to #626

@lonix1
Copy link

lonix1 commented Dec 29, 2021

@douglasg14b I have the same use case as you, and the approach you mentioned above (get entity states, handle before, call SaveChanges, handle after) was the way I used to do it in "old" EF.

The workaround mentioned above is very interesting. Did you do it that way? There could be a race condition where some other code (maybe the domain event handler) calls SaveChanges again before you call AcceptAllChanges?

I'm trying to decide between these approaches:

  1. the manual way described above by overriding SaveChanges
  2. the AcceptAllChanges way described by ajcvickers
  3. using SavingChanges and SavedChanges events
  4. the SavedChangesInterceptor interceptor

@roji
Copy link
Member

roji commented Dec 29, 2021

For why there's no race condition (as mentioned above by @lonix1), see dotnet/EntityFramework.Docs#3645.

@lonix1
Copy link

lonix1 commented Dec 29, 2021

Hey @douglasg14b, given roji's comments in that linked issue, my feeling now is that the SaveChangesInterceptor approach is the cleanest/easiest - assuming you can redesign (if not, I like ajcvicker's approach). It simplified my code, and is much easier to test.

@douglasg14b
Copy link
Author

douglasg14b commented Dec 29, 2021

@lonix1 I am not considering race conditions since each scope has it's own DbContext, I don't do anything with a context instance in parallel. It's always in serial, which means any other calls occur after the previous one has completed anyways.

I am handling this with a SaveChanges override.

This is what I've thrown together, it works thus far, but I'm sure isn't very elegant. I haven't actually tested the transaction yet, hopefully the setup doesn't have errors...

        public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
        {
            // We already started, and this is a savechanges call made after we have already entered this fuction
            if(Database.CurrentTransaction != null) // <----- hack to capture nested savechanges calls
            {
                return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
            }

            OnBeforeSaveChanges(); // <---- Does all the observe-ability stuff, and sets up necessary state
            int result = 0;

            using (var transaction = Database.BeginTransaction())
            {
                try
                {
                    result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);

                    IStatus afterSaveStatus = await OnAfterSavedChangesAsync(); // <---- Perform all the post-save changes actions, this can also call SaveChanges
                    if (afterSaveStatus.HasErrors) throw new Exception(afterSaveStatus.ToString()); // <--- Abort if errors 

                    await transaction.CommitAsync();
                }
                catch (Exception ex)
                {
                    // Log?
                    await transaction.RollbackAsync();

                    throw; // Rethrow the exception so callers can handle
                }
            }

            return result;
        }
            

@lonix1
Copy link

lonix1 commented Dec 30, 2021

@douglasg14b I like it. I used to do something very similar in an older EF version - I don't know why you think it's inelegant, it was the standard way for a long time. I assume you still need to use ajcvcker's AcceptAllChanges trick at the callsite. You might reconsider the interceptor approach someday if you outgrow your current approach, it allows you to move most of this to a separate easily-tested class.

@AndriySvyryd AndriySvyryd changed the title How to determine which entities where modified on "SavedChanges" callback? Add affected entries to "SavedChanges" callback Jan 14, 2022
@ajcvickers ajcvickers removed their assignment Aug 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants