async Task DoSave(IEntity entity, SaveBehaviour behaviour) { var mode = entity.IsNew ? SaveMode.Insert : SaveMode.Update; var asEntity = entity as Entity; if (mode == SaveMode.Update && (asEntity._ClonedFrom?.IsStale == true) && AnyOpenTransaction()) { throw new InvalidOperationException("This " + entity.GetType().Name + " instance in memory is out-of-date. " + "A clone of it is already updated in the transaction. It is not allowed to update the same instance multiple times in a transaction, because then the earlier updates would be overwriten by the older state of the instance in memory. \r\n\r\n" + @"BAD: Database.Update(myObject, x=> x.P1 = ...); // Note: this could also be nested inside another method that's called here instead. Database.Update(myObject, x=> x.P2 = ...); GOOD: Database.Update(myObject, x=> x.P1 = ...); myObject = Database.Reload(myObject); Database.Update(myObject, x=> x.P2 = ...);"); } if (Entity.Services.IsImmutable(entity)) { throw new ArgumentException("An immutable record must be cloned before any modifications can be applied on it. " + $"Type={entity.GetType().FullName}, Id={entity.GetId()}."); } var dataProvider = GetProvider(entity); if (!IsSet(behaviour, SaveBehaviour.BypassValidation)) { await Entity.Services.RaiseOnValidating(entity as Entity, EventArgs.Empty); await entity.Validate(); } else if (!dataProvider.SupportValidationBypassing()) { throw new ArgumentException(dataProvider.GetType().Name + " does not support bypassing validation."); } #region Raise saving event if (!IsSet(behaviour, SaveBehaviour.BypassSaving)) { var savingArgs = new System.ComponentModel.CancelEventArgs(); await Entity.Services.RaiseOnSaving(entity, savingArgs); if (savingArgs.Cancel) { Cache.Remove(entity); return; } } #endregion if (!IsSet(behaviour, SaveBehaviour.BypassLogging)) { if (mode == SaveMode.Insert) { await Audit.LogInsert(entity); } else { await Audit.LogUpdate(entity); } } await dataProvider.Save(entity); Cache.UpdateRowVersion(entity); if (mode == SaveMode.Update && asEntity?._ClonedFrom != null && AnyOpenTransaction()) { asEntity._ClonedFrom.IsStale = true; asEntity.IsStale = false; } if (mode == SaveMode.Insert) { Entity.Services.SetSaved(entity); } Cache.Remove(entity); if (Transaction.Current != null) { Transaction.Current.TransactionCompleted += (s, e) => { Cache.Remove(entity); } } ; DbTransactionScope.Root?.OnTransactionCompleted(() => Cache.Remove(entity)); await Updated.Raise(entity); if (!IsSet(behaviour, SaveBehaviour.BypassSaved)) { await Entity.Services.RaiseOnSaved(entity, new SaveEventArgs(mode)); } // OnSaved event handler might have read the object again and put it in the cache, which would // create invalid CachedReference objects. Cache.Remove(entity); }