private void logChanges(TPrincipal principal) { var logger = new ChangeLogger <TChangeSet, TPrincipal>(context, factory, filter); // This returns the log objects, but they are not attached to the context // so the context change tracker hasn't noticed them var oven = logger.Log(context.ObjectStateManager); // So when we accept changes, we are only accepting the changes from the // original changes - the context hasn't yet detected the log changes context.AcceptAllChanges(); // This code then attaches the log objects to the context if (oven.HasChangeSet) { // First do any deferred log value calculations. // See PropertyChange.Bake for more information TChangeSet changeSet = oven.Bake(DateTime.Now, principal); context.AddChangeSet(changeSet); } }
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); }
/// <summary> /// Run in PreCommit phase to create Framelog module, IOven instance /// </summary> /// <param name="principal"></param> public void LogChanges(TPrincipal principal) { logger = new ChangeLogger <TChangeSet, TPrincipal>(contextInfo.ObjectContext, factory, filter); oven = logger.Log(contextInfo.ObjectContext.ObjectStateManager); }