protected async Task <ISaveResult <TChangeSet> > saveChangesAsync(TPrincipal principal, ITransactionProvider transactionProvider, CancellationToken cancellationToken) { if (!Enabled) { return(new SaveResult <TChangeSet, TPrincipal>(await context.SaveAndAcceptChangesAsync(cancellationToken: cancellationToken))); } var result = new SaveResult <TChangeSet, TPrincipal>(); // We want to split saving and logging into two steps, so that when we // generate the log objects the database has already assigned IDs to new // objects. Then we can log about them meaningfully. So we wrap it in a // transaction so that even though there are two saves, the change is still // atomic. cancellationToken.ThrowIfCancellationRequested(); await transactionProvider.InTransactionAsync(async() => { var logger = new ChangeLogger <TChangeSet, TPrincipal>(context, factory, filter, serializer); var oven = (IOven <TChangeSet, TPrincipal>)null; // First we detect all the changes, but we do not save or accept the changes // (i.e. we keep our record of them). cancellationToken.ThrowIfCancellationRequested(); context.DetectChanges(); // Then we save and accept the changes, which invokes the standard EntityFramework // DbContext.SaveChanges(), including any custom user logic the end-user has defined. // Eventually, DbContext.InternalContext.ObjectContext.SaveChanges() will be invoked // and then the delegate below is called back to prepare the log objects/changes. cancellationToken.ThrowIfCancellationRequested(); result.AffectedObjectCount = await context.SaveAndAcceptChangesAsync(cancellationToken: cancellationToken, onSavingChanges: (sender, args) => { // This is invoked just moments before EntityFramework accepts the original changes. // Now is our best oppertunity to create the log objects, which will not yet be attached // to the context. They are unattached so that the context change tracker won't noticed // them when accepting the original changes. cancellationToken.ThrowIfCancellationRequested(); oven = logger.Log(context.ObjectStateManager); // NOTE: This is the last chance to cancel the save. After this, the original changes // will have been accepted and it will be too late to stop now (see comment below) cancellationToken.ThrowIfCancellationRequested(); } ); // NOTE: From this point in, we stop honoring the cancellation token. // Why? because if we did, you would end up persisted object changes without any associated logging. // In the interest of data integrity, we either persist the object changes + logging, or nothing at all. // If the oven is not set here, then DbContext.SaveChanges() did not call our delegate back // when accepting the original changes. Without the oven, we cannot bake the logged changes. if (oven == null) { throw new ChangesNotDetectedException(); } // Finally, we attach the previously prepared log objects to the context (and save/accept them) if (oven.HasChangeSet) { // First do any deferred log value calculations. // (see PropertyChange.Bake for more information) // Then detect all the log changes that were previously deferred result.ChangeSet = oven.Bake(DateTime.Now, principal); context.AddChangeSet(result.ChangeSet); context.DetectChanges(); // Then we save and accept the changes that result from creating the log objects // NOTE: We do not use SaveAndAcceptChanges() here because we are not interested in going // through DbContext.SaveChanges() and invoking end-users custom logic. await context.SaveChangesAsync(SaveOptions.AcceptAllChangesAfterSave); } }); return(result); }