using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Diagnostics; namespace System.Data.Linq { using System.Data.Linq.Mapping; using System.Data.Linq.Provider; /// /// Describes the type of change the entity will undergo when submitted to the database. /// public enum ChangeAction { /// /// The entity will not be submitted. /// None = 0, /// /// The entity will be deleted. /// Delete, /// /// The entity will be inserted. /// Insert, /// /// The entity will be updated. /// Update } internal class ChangeProcessor { CommonDataServices services; DataContext context; ChangeTracker tracker; ChangeDirector changeDirector; EdgeMap currentParentEdges; EdgeMap originalChildEdges; ReferenceMap originalChildReferences; internal ChangeProcessor(CommonDataServices services, DataContext context) { this.services = services; this.context = context; this.tracker = services.ChangeTracker; this.changeDirector = services.ChangeDirector; this.currentParentEdges = new EdgeMap(); this.originalChildEdges = new EdgeMap(); this.originalChildReferences = new ReferenceMap(); } internal void SubmitChanges(ConflictMode failureMode) { this.TrackUntrackedObjects(); // Must apply inferred deletions only after any untracked objects // are tracked this.ApplyInferredDeletions(); this.BuildEdgeMaps(); var list = this.GetOrderedList(); ValidateAll(list); int numUpdatesAttempted = 0; ChangeConflictSession conflictSession = new ChangeConflictSession(this.context); List conflicts = new List(); List deletedItems = new List(); List insertedItems = new List(); List syncDependentItems = new List(); foreach (TrackedObject item in list) { try { if (item.IsNew) { if (item.SynchDependentData()) { syncDependentItems.Add(item); } changeDirector.Insert(item); // store all inserted items for post processing insertedItems.Add(item); } else if (item.IsDeleted) { // Delete returns 1 if the delete was successfull, 0 if the row exists // but wasn't deleted due to an OC conflict, or -1 if the row was // deleted by another context (no OC conflict in this case) numUpdatesAttempted++; int ret = changeDirector.Delete(item); if (ret == 0) { conflicts.Add(new ObjectChangeConflict(conflictSession, item, false)); } else { // store all deleted items for post processing deletedItems.Add(item); } } else if (item.IsPossiblyModified) { if (item.SynchDependentData()) { syncDependentItems.Add(item); } if (item.IsModified) { CheckForInvalidChanges(item); numUpdatesAttempted++; if (changeDirector.Update(item) <= 0) { conflicts.Add(new ObjectChangeConflict(conflictSession, item)); } } } } catch (ChangeConflictException) { conflicts.Add(new ObjectChangeConflict(conflictSession, item)); } if (conflicts.Count > 0 && failureMode == ConflictMode.FailOnFirstConflict) { break; } } // if we have accumulated any failed updates, throw the exception now if (conflicts.Count > 0) { // First we need to rollback any value that have already been auto-[....]'d, since the values are no longer valid on the server changeDirector.RollbackAutoSync(); // Also rollback any dependent items that were [....]'d, since their parent values may have been rolled back foreach (TrackedObject syncDependentItem in syncDependentItems) { Debug.Assert(syncDependentItem.IsNew || syncDependentItem.IsPossiblyModified, "SynchDependent data should only be rolled back for new and modified objects."); syncDependentItem.SynchDependentData(); } this.context.ChangeConflicts.Fill(conflicts); throw CreateChangeConflictException(numUpdatesAttempted, conflicts.Count); } else { // No conflicts occurred, so we don't need to save the rollback values anymore changeDirector.ClearAutoSyncRollback(); } // Only after all updates have been sucessfully processed do we want to make // post processing modifications to the objects and/or cache state. PostProcessUpdates(insertedItems, deletedItems); } private void PostProcessUpdates(List insertedItems, List deletedItems) { // perform post delete processing foreach (TrackedObject deletedItem in deletedItems) { // remove deleted item from identity cache this.services.RemoveCachedObjectLike(deletedItem.Type, deletedItem.Original); ClearForeignKeyReferences(deletedItem); } // perform post insert processing foreach (TrackedObject insertedItem in insertedItems) { object lookup = this.services.InsertLookupCachedObject(insertedItem.Type, insertedItem.Current); if (lookup != insertedItem.Current) { throw new DuplicateKeyException(insertedItem.Current, Strings.DatabaseGeneratedAlreadyExistingKey); } insertedItem.InitializeDeferredLoaders(); } } /// /// Clears out the foreign key values and parent object references for deleted objects on the child side of a relationship. /// For bi-directional relationships, also performs the following fixup: /// - for 1:N we remove the deleted entity from the opposite EntitySet or collection /// - for 1:1 we null out the back reference /// private void ClearForeignKeyReferences(TrackedObject to) { Debug.Assert(to.IsDeleted, "Foreign key reference cleanup should only happen on Deleted objects."); foreach (MetaAssociation assoc in to.Type.Associations) { if (assoc.IsForeignKey) { // If there is a member on the other side referring back to us (i.e. this is a bi-directional relationship), // we want to do a cache lookup to find the other side, then will remove ourselves from that collection. // This cache lookup is only possible if the other key is the primary key, since that is the only way items can be found in the cache. if (assoc.OtherMember != null && assoc.OtherKeyIsPrimaryKey) { Debug.Assert(assoc.OtherMember.IsAssociation, "OtherMember of the association is expected to also be an association."); // Search the cache for the target of the association, since // it might not be loaded on the object being deleted, and we // don't want to force a load. object[] keyValues = CommonDataServices.GetForeignKeyValues(assoc, to.Current); object cached = this.services.IdentityManager.Find(assoc.OtherType, keyValues); if (cached != null) { if (assoc.OtherMember.Association.IsMany) { // Note that going through the IList interface handles // EntitySet as well as POCO collections that implement IList // and are not FixedSize. System.Collections.IList collection = assoc.OtherMember.MemberAccessor.GetBoxedValue(cached) as System.Collections.IList; if (collection != null && !collection.IsFixedSize) { collection.Remove(to.Current); // Explicitly clear the foreign key values and parent object reference ClearForeignKeysHelper(assoc, to.Current); } } else { // Null out the other association. Since this is a 1:1 association, // we're not concerned here with causing a deferred load, since the // target is already cached (since we're deleting it). assoc.OtherMember.MemberAccessor.SetBoxedValue(ref cached, null); // Explicitly clear the foreign key values and parent object reference ClearForeignKeysHelper(assoc, to.Current); } } // else the item was not found in the cache, so there is no fixup that has to be done // We are explicitly not calling ClearForeignKeysHelper because it breaks existing shipped behavior and we want to maintain backward compatibility } else { // This is a unidirectional relationship or we have no way to look up the other side in the cache, so just clear our own side ClearForeignKeysHelper(assoc, to.Current); } } // else this is not the 1-side (foreign key) of the relationship, so there is nothing for us to do } } // Ensure the the member and foreign keys are nulled so that after trackedInstance is deleted, // the object does not appear to be associated with the other side anymore. This prevents the deleted object // from referencing objects still in the cache, but also will prevent the related object from being implicitly loaded private static void ClearForeignKeysHelper(MetaAssociation assoc, object trackedInstance) { Debug.Assert(assoc.IsForeignKey, "Foreign key clearing should only happen on foreign key side of the association."); Debug.Assert(assoc.ThisMember.IsAssociation, "Expected ThisMember of an association to always be an association."); // If this member is one of our deferred loaders, and it does not already have a value, explicitly set the deferred source to // null so that when we set the association member itself to null later, it doesn't trigger an implicit load. // This is only necessary if the value has not already been assigned or set, because otherwise we won't implicitly load anyway when the member is accessed. MetaDataMember thisMember = assoc.ThisMember; if (thisMember.IsDeferred && !(thisMember.StorageAccessor.HasAssignedValue(trackedInstance) || thisMember.StorageAccessor.HasLoadedValue(trackedInstance))) { // If this is a deferred member, set the value directly in the deferred accessor instead of going // through the normal member accessor, so that we don't trigger an implicit load. thisMember.DeferredSourceAccessor.SetBoxedValue(ref trackedInstance, null); } // Notify the object that the relationship should be considered deleted. // This allows the object to do its own fixup even when we can't do it automatically. thisMember.MemberAccessor.SetBoxedValue(ref trackedInstance, null); // Also set the foreign key values to null if possible for (int i = 0, n = assoc.ThisKey.Count; i < n; i++) { MetaDataMember thisKey = assoc.ThisKey[i]; if (thisKey.CanBeNull) { thisKey.StorageAccessor.SetBoxedValue(ref trackedInstance, null); } } } private static void ValidateAll(IEnumerable list) { foreach (var item in list) { if (item.IsNew) { item.SynchDependentData(); if (item.Type.HasAnyValidateMethod) { SendOnValidate(item.Type, item, ChangeAction.Insert); } } else if (item.IsDeleted) { if (item.Type.HasAnyValidateMethod) { SendOnValidate(item.Type, item, ChangeAction.Delete); } } else if (item.IsPossiblyModified) { item.SynchDependentData(); if (item.IsModified && item.Type.HasAnyValidateMethod) { SendOnValidate(item.Type, item, ChangeAction.Update); } } } } private static void SendOnValidate(MetaType type, TrackedObject item, ChangeAction changeAction) { if (type != null) { SendOnValidate(type.InheritanceBase, item, changeAction); if (type.OnValidateMethod != null) { try { type.OnValidateMethod.Invoke(item.Current, new object[] { changeAction }); } catch (TargetInvocationException tie) { if (tie.InnerException != null) { throw tie.InnerException; } throw; } } } } internal string GetChangeText() { this.ObserveUntrackedObjects(); // Must apply inferred deletions only after any untracked objects // are tracked this.ApplyInferredDeletions(); this.BuildEdgeMaps(); // append change text only StringBuilder changeText = new StringBuilder(); foreach (TrackedObject item in this.GetOrderedList()) { if (item.IsNew) { item.SynchDependentData(); changeDirector.AppendInsertText(item, changeText); } else if (item.IsDeleted) { changeDirector.AppendDeleteText(item, changeText); } else if (item.IsPossiblyModified) { item.SynchDependentData(); if (item.IsModified) { changeDirector.AppendUpdateText(item, changeText); } } } return changeText.ToString(); } internal ChangeSet GetChangeSet() { List newEntities = new List(); List deletedEntities = new List(); List changedEntities = new List(); this.ObserveUntrackedObjects(); // Must apply inferred deletions only after any untracked objects // are tracked this.ApplyInferredDeletions(); foreach (TrackedObject item in this.tracker.GetInterestingObjects()) { if (item.IsNew) { item.SynchDependentData(); newEntities.Add(item.Current); } else if (item.IsDeleted) { deletedEntities.Add(item.Current); } else if (item.IsPossiblyModified) { item.SynchDependentData(); if (item.IsModified) { changedEntities.Add(item.Current); } } } return new ChangeSet(newEntities.AsReadOnly(), deletedEntities.AsReadOnly(), changedEntities.AsReadOnly()); } // verify that primary key and db-generated values have not changed private static void CheckForInvalidChanges(TrackedObject tracked) { foreach (MetaDataMember mem in tracked.Type.PersistentDataMembers) { if (mem.IsPrimaryKey || mem.IsDbGenerated || mem.IsVersion) { if (tracked.HasChangedValue(mem)) { if (mem.IsPrimaryKey) { throw Error.IdentityChangeNotAllowed(mem.Name, tracked.Type.Name); } else { throw Error.DbGeneratedChangeNotAllowed(mem.Name, tracked.Type.Name); } } } } } /// /// Create an ChangeConflictException with the best message /// static private ChangeConflictException CreateChangeConflictException(int totalUpdatesAttempted, int failedUpdates) { string msg = Strings.RowNotFoundOrChanged; if (totalUpdatesAttempted > 1) { msg = Strings.UpdatesFailedMessage(failedUpdates, totalUpdatesAttempted); } return new ChangeConflictException(msg); } internal void TrackUntrackedObjects() { Dictionary visited = new Dictionary(); // search for untracked new objects List items = new List(this.tracker.GetInterestingObjects()); foreach (TrackedObject item in items) { this.TrackUntrackedObjects(item.Type, item.Current, visited); } } internal void ApplyInferredDeletions() { foreach (TrackedObject item in this.tracker.GetInterestingObjects()) { if (item.CanInferDelete()) { // based on DeleteOnNull specifications on the item's associations, // a deletion can be inferred for this item. The actual state transition // is dependent on the current item state. if (item.IsNew) { item.ConvertToRemoved(); } else if (item.IsPossiblyModified || item.IsModified) { item.ConvertToDeleted(); } } } } private void TrackUntrackedObjects(MetaType type, object item, Dictionary visited) { if (!visited.ContainsKey(item)) { visited.Add(item, item); TrackedObject tracked = this.tracker.GetTrackedObject(item); if (tracked == null) { tracked = this.tracker.Track(item); tracked.ConvertToNew(); } else if (tracked.IsDead || tracked.IsRemoved) { // ignore return; } // search parents (objects we are dependent on) foreach (RelatedItem parent in this.services.GetParents(type, item)) { this.TrackUntrackedObjects(parent.Type, parent.Item, visited); } // synch up primary key if (tracked.IsNew) { tracked.InitializeDeferredLoaders(); if (!tracked.IsPendingGeneration(tracked.Type.IdentityMembers)) { tracked.SynchDependentData(); object cached = this.services.InsertLookupCachedObject(tracked.Type, item); if (cached != item) { TrackedObject cachedTracked = this.tracker.GetTrackedObject(cached); Debug.Assert(cachedTracked != null); if (cachedTracked.IsDeleted || cachedTracked.CanInferDelete()) { // adding new object with same ID as object being deleted.. turn into modified tracked.ConvertToPossiblyModified(cachedTracked.Original); // turn deleted to dead... cachedTracked.ConvertToDead(); this.services.RemoveCachedObjectLike(tracked.Type, item); this.services.InsertLookupCachedObject(tracked.Type, item); } else if (!cachedTracked.IsDead) { throw new DuplicateKeyException(item, Strings.CantAddAlreadyExistingKey); } } } else { // we may have a generated PK, however we set the PK on this new item to // match a deleted item object cached = this.services.GetCachedObjectLike(tracked.Type, item); if (cached != null) { TrackedObject cachedTracked = this.tracker.GetTrackedObject(cached); Debug.Assert(cachedTracked != null); if (cachedTracked.IsDeleted || cachedTracked.CanInferDelete()) { // adding new object with same ID as object being deleted.. turn into modified tracked.ConvertToPossiblyModified(cachedTracked.Original); // turn deleted to dead... cachedTracked.ConvertToDead(); this.services.RemoveCachedObjectLike(tracked.Type, item); this.services.InsertLookupCachedObject(tracked.Type, item); } } } } // search children (objects that are dependent on us) foreach (RelatedItem child in this.services.GetChildren(type, item)) { this.TrackUntrackedObjects(child.Type, child.Item, visited); } } } internal void ObserveUntrackedObjects() { Dictionary visited = new Dictionary(); List items = new List(this.tracker.GetInterestingObjects()); foreach (TrackedObject item in items) { this.ObserveUntrackedObjects(item.Type, item.Current, visited); } } private void ObserveUntrackedObjects(MetaType type, object item, Dictionary visited) { if (!visited.ContainsKey(item)) { visited.Add(item, item); TrackedObject tracked = this.tracker.GetTrackedObject(item); if (tracked == null) { tracked = this.tracker.Track(item); tracked.ConvertToNew(); } else if (tracked.IsDead || tracked.IsRemoved) { // ignore return; } // search parents (objects we are dependent on) foreach (RelatedItem parent in this.services.GetParents(type, item)) { this.ObserveUntrackedObjects(parent.Type, parent.Item, visited); } // synch up primary key unless its generated. if (tracked.IsNew) { if (!tracked.IsPendingGeneration(tracked.Type.IdentityMembers)) { tracked.SynchDependentData(); } } // search children (objects that are dependent on us) foreach (RelatedItem child in this.services.GetChildren(type, item)) { this.ObserveUntrackedObjects(child.Type, child.Item, visited); } } } private TrackedObject GetOtherItem(MetaAssociation assoc, object instance) { if (instance == null) return null; object other = null; // Don't load unloaded references if (assoc.ThisMember.StorageAccessor.HasAssignedValue(instance) || assoc.ThisMember.StorageAccessor.HasLoadedValue(instance) ) { other = assoc.ThisMember.MemberAccessor.GetBoxedValue(instance); } else if (assoc.OtherKeyIsPrimaryKey) { // Maybe it's in the cache, but not yet attached through reference. object[] foreignKeys = CommonDataServices.GetForeignKeyValues(assoc, instance); other = this.services.GetCachedObject(assoc.OtherType, foreignKeys); } // else the other key is not the primary key so there is no way to try to look it up return (other != null) ? this.tracker.GetTrackedObject(other) : null; } private bool HasAssociationChanged(MetaAssociation assoc, TrackedObject item) { if (item.Original != null && item.Current != null) { if (assoc.ThisMember.StorageAccessor.HasAssignedValue(item.Current) || assoc.ThisMember.StorageAccessor.HasLoadedValue(item.Current) ) { return this.GetOtherItem(assoc, item.Current) != this.GetOtherItem(assoc, item.Original); } else { object[] currentFKs = CommonDataServices.GetForeignKeyValues(assoc, item.Current); object[] originaFKs = CommonDataServices.GetForeignKeyValues(assoc, item.Original); for (int i = 0, n = currentFKs.Length; i < n; i++) { if (!object.Equals(currentFKs[i], originaFKs[i])) return true; } } } return false; } private void BuildEdgeMaps() { this.currentParentEdges.Clear(); this.originalChildEdges.Clear(); this.originalChildReferences.Clear(); List list = new List(this.tracker.GetInterestingObjects()); foreach (TrackedObject item in list) { bool isNew = item.IsNew; MetaType mt = item.Type; foreach (MetaAssociation assoc in mt.Associations) { if (assoc.IsForeignKey) { TrackedObject otherItem = this.GetOtherItem(assoc, item.Current); TrackedObject dbOtherItem = this.GetOtherItem(assoc, item.Original); bool pointsToDeleted = (otherItem != null && otherItem.IsDeleted) || (dbOtherItem != null && dbOtherItem.IsDeleted); bool pointsToNew = (otherItem != null && otherItem.IsNew); if (isNew || pointsToDeleted || pointsToNew || this.HasAssociationChanged(assoc, item)) { if (otherItem != null) { this.currentParentEdges.Add(assoc, item, otherItem); } if (dbOtherItem != null) { if (assoc.IsUnique) { this.originalChildEdges.Add(assoc, dbOtherItem, item); } this.originalChildReferences.Add(dbOtherItem, item); } } } } } } enum VisitState { Before, After } private List GetOrderedList() { var objects = this.tracker.GetInterestingObjects().ToList(); // give list an initial order (most likely correct order) to avoid deadlocks in server var range = Enumerable.Range(0, objects.Count).ToList(); range.Sort((int x, int y) => Compare(objects[x], x, objects[y], y)); var ordered = range.Select(i => objects[i]).ToList(); // permute order if constraint dependencies requires some changes to come before others var visited = new Dictionary(); var list = new List(); foreach (TrackedObject item in ordered) { this.BuildDependencyOrderedList(item, list, visited); } return list; } private static int Compare(TrackedObject x, int xOrdinal, TrackedObject y, int yOrdinal) { // deal with possible nulls if (x == y) { return 0; } if (x == null) { return -1; } else if (y == null) { return 1; } // first order by action: Inserts first, Updates, Deletes last int xAction = x.IsNew ? 0 : x.IsDeleted ? 2 : 1; int yAction = y.IsNew ? 0 : y.IsDeleted ? 2 : 1; if (xAction < yAction) { return -1; } else if (xAction > yAction) { return 1; } // no need to order inserts (PK's may not even exist) if (x.IsNew) { // keep original order return xOrdinal.CompareTo(yOrdinal); } // second order by type if (x.Type != y.Type) { return string.CompareOrdinal(x.Type.Type.FullName, y.Type.Type.FullName); } // lastly, order by PK values int result = 0; foreach (MetaDataMember mm in x.Type.IdentityMembers) { object xValue = mm.StorageAccessor.GetBoxedValue(x.Current); object yValue = mm.StorageAccessor.GetBoxedValue(y.Current); if (xValue == null) { if (yValue != null) { return -1; } } else { IComparable xc = xValue as IComparable; if (xc != null) { result = xc.CompareTo(yValue); if (result != 0) { return result; } } } } // they are the same? leave in original order return xOrdinal.CompareTo(yOrdinal); } private void BuildDependencyOrderedList(TrackedObject item, List list, Dictionary visited) { VisitState state; if (visited.TryGetValue(item, out state)) { if (state == VisitState.Before) { throw Error.CycleDetected(); } return; } visited[item] = VisitState.Before; if (item.IsInteresting) { if (item.IsDeleted) { // if 'item' is deleted // all objects that used to refer to 'item' must be ordered before item foreach (TrackedObject other in this.originalChildReferences[item]) { if (other != item) { this.BuildDependencyOrderedList(other, list, visited); } } } else { // if 'item' is new or changed // for all objects 'other' that 'item' refers to along association 'assoc' // if 'other' is new then 'other' must be ordered before 'item' // if 'assoc' is pure one-to-one and some other item 'prevItem' used to refer to 'other' // then 'prevItem' must be ordered before 'item' foreach (MetaAssociation assoc in item.Type.Associations) { if (assoc.IsForeignKey) { TrackedObject other = this.currentParentEdges[assoc, item]; if (other != null) { if (other.IsNew) { // if other is new, visit other first (since item's FK depends on it) if (other != item || item.Type.DBGeneratedIdentityMember != null) { this.BuildDependencyOrderedList(other, list, visited); } } else if ((assoc.IsUnique || assoc.ThisKeyIsPrimaryKey)) { TrackedObject prevItem = this.originalChildEdges[assoc, other]; if (prevItem != null && other != item) { this.BuildDependencyOrderedList(prevItem, list, visited); } } } } } } list.Add(item); } visited[item] = VisitState.After; } class EdgeMap { Dictionary> associations; internal EdgeMap() { this.associations = new Dictionary>(); } internal void Add(MetaAssociation assoc, TrackedObject from, TrackedObject to) { Dictionary pairs; if (!associations.TryGetValue(assoc, out pairs)) { pairs = new Dictionary(); associations.Add(assoc, pairs); } pairs.Add(from, to); } internal TrackedObject this[MetaAssociation assoc, TrackedObject from] { get { Dictionary pairs; if (associations.TryGetValue(assoc, out pairs)) { TrackedObject to; if (pairs.TryGetValue(from, out to)) { return to; } } return null; } } internal void Clear() { this.associations.Clear(); } } class ReferenceMap { Dictionary> references; internal ReferenceMap() { this.references = new Dictionary>(); } internal void Add(TrackedObject from, TrackedObject to) { List refs; if (!references.TryGetValue(from, out refs)) { refs = new List(); references.Add(from, refs); } if (!refs.Contains(to)) refs.Add(to); } internal IEnumerable this[TrackedObject from] { get { List refs; if (references.TryGetValue(from, out refs)) { return refs; } return Empty; } } internal void Clear() { this.references.Clear(); } private static TrackedObject[] Empty = new TrackedObject[] { }; } } }