public int SaveChanges(TPrincipal principal, TransactionOptions transactionOptions) { if (!Enabled) { return(context.SaveChanges(detectAndAccept)); } int result = 0; // 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. using (var scope = new TransactionScope(TransactionScopeOption.Required, transactionOptions)) { // First we save all the changes, but we do not accept the changes // (i.e. we keep our record of them). result = context.SaveChanges(SaveOptions.DetectChangesBeforeSave); // Then, based on that record of changes we create log objects for // them and then accept the first set of changes. logChanges(principal); // Then we save the changes that result from creating the log objects, // and accept this second set of changes. context.SaveChanges(detectAndAccept); scope.Complete(); } return(result); }
protected ISaveResult <TChangeSet> saveChanges(TPrincipal principal, ITransactionProvider transactionProvider) { if (!Enabled) { return(new SaveResult <TChangeSet, TPrincipal>(context.SaveAndAcceptChanges())); } 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. transactionProvider.InTransaction(() => { 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). 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. result.AffectedObjectCount = context.SaveAndAcceptChanges((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. oven = logger.Log(context.ObjectStateManager); }); // 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. context.SaveChanges(SaveOptions.AcceptAllChangesAfterSave); } }); return(result); }