Updates referencesource to .NET 4.7
[mono-project.git] / mcs / class / referencesource / System.Data.Linq / ChangeProcessor.cs
blob0affa3cb60ade99aa0ed6733d62098a248a26775
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Reflection;
5 using System.Text;
6 using System.Diagnostics;
8 namespace System.Data.Linq {
9 using System.Data.Linq.Mapping;
10 using System.Data.Linq.Provider;
12 /// <summary>
13 /// Describes the type of change the entity will undergo when submitted to the database.
14 /// </summary>
15 public enum ChangeAction {
16 /// <summary>
17 /// The entity will not be submitted.
18 /// </summary>
19 None = 0,
20 /// <summary>
21 /// The entity will be deleted.
22 /// </summary>
23 Delete,
24 /// <summary>
25 /// The entity will be inserted.
26 /// </summary>
27 Insert,
28 /// <summary>
29 /// The entity will be updated.
30 /// </summary>
31 Update
34 internal class ChangeProcessor {
35 CommonDataServices services;
36 DataContext context;
37 ChangeTracker tracker;
38 ChangeDirector changeDirector;
39 EdgeMap currentParentEdges;
40 EdgeMap originalChildEdges;
41 ReferenceMap originalChildReferences;
43 internal ChangeProcessor(CommonDataServices services, DataContext context) {
44 this.services = services;
45 this.context = context;
46 this.tracker = services.ChangeTracker;
47 this.changeDirector = services.ChangeDirector;
48 this.currentParentEdges = new EdgeMap();
49 this.originalChildEdges = new EdgeMap();
50 this.originalChildReferences = new ReferenceMap();
53 internal void SubmitChanges(ConflictMode failureMode) {
54 this.TrackUntrackedObjects();
55 // Must apply inferred deletions only after any untracked objects
56 // are tracked
57 this.ApplyInferredDeletions();
58 this.BuildEdgeMaps();
60 var list = this.GetOrderedList();
62 ValidateAll(list);
64 int numUpdatesAttempted = 0;
65 ChangeConflictSession conflictSession = new ChangeConflictSession(this.context);
66 List<ObjectChangeConflict> conflicts = new List<ObjectChangeConflict>();
67 List<TrackedObject> deletedItems = new List<TrackedObject>();
68 List<TrackedObject> insertedItems = new List<TrackedObject>();
69 List<TrackedObject> syncDependentItems = new List<TrackedObject>();
71 foreach (TrackedObject item in list) {
72 try {
73 if (item.IsNew) {
74 if (item.SynchDependentData()) {
75 syncDependentItems.Add(item);
77 changeDirector.Insert(item);
78 // store all inserted items for post processing
79 insertedItems.Add(item);
81 else if (item.IsDeleted) {
82 // Delete returns 1 if the delete was successfull, 0 if the row exists
83 // but wasn't deleted due to an OC conflict, or -1 if the row was
84 // deleted by another context (no OC conflict in this case)
85 numUpdatesAttempted++;
86 int ret = changeDirector.Delete(item);
87 if (ret == 0) {
88 conflicts.Add(new ObjectChangeConflict(conflictSession, item, false));
90 else {
91 // store all deleted items for post processing
92 deletedItems.Add(item);
95 else if (item.IsPossiblyModified) {
96 if (item.SynchDependentData()) {
97 syncDependentItems.Add(item);
99 if (item.IsModified) {
100 CheckForInvalidChanges(item);
101 numUpdatesAttempted++;
102 if (changeDirector.Update(item) <= 0) {
103 conflicts.Add(new ObjectChangeConflict(conflictSession, item));
108 catch (ChangeConflictException) {
109 conflicts.Add(new ObjectChangeConflict(conflictSession, item));
111 if (conflicts.Count > 0 && failureMode == ConflictMode.FailOnFirstConflict) {
112 break;
116 // if we have accumulated any failed updates, throw the exception now
117 if (conflicts.Count > 0) {
118 // First we need to rollback any value that have already been auto-sync'd, since the values are no longer valid on the server
119 changeDirector.RollbackAutoSync();
120 // Also rollback any dependent items that were sync'd, since their parent values may have been rolled back
121 foreach (TrackedObject syncDependentItem in syncDependentItems) {
122 Debug.Assert(syncDependentItem.IsNew || syncDependentItem.IsPossiblyModified, "SynchDependent data should only be rolled back for new and modified objects.");
123 syncDependentItem.SynchDependentData();
125 this.context.ChangeConflicts.Fill(conflicts);
126 throw CreateChangeConflictException(numUpdatesAttempted, conflicts.Count);
128 else {
129 // No conflicts occurred, so we don't need to save the rollback values anymore
130 changeDirector.ClearAutoSyncRollback();
133 // Only after all updates have been sucessfully processed do we want to make
134 // post processing modifications to the objects and/or cache state.
135 PostProcessUpdates(insertedItems, deletedItems);
138 private void PostProcessUpdates(List<TrackedObject> insertedItems, List<TrackedObject> deletedItems) {
139 // perform post delete processing
140 foreach (TrackedObject deletedItem in deletedItems) {
141 // remove deleted item from identity cache
142 this.services.RemoveCachedObjectLike(deletedItem.Type, deletedItem.Original);
143 ClearForeignKeyReferences(deletedItem);
146 // perform post insert processing
147 foreach (TrackedObject insertedItem in insertedItems) {
148 object lookup = this.services.InsertLookupCachedObject(insertedItem.Type, insertedItem.Current);
149 if (lookup != insertedItem.Current) {
150 throw new DuplicateKeyException(insertedItem.Current, Strings.DatabaseGeneratedAlreadyExistingKey);
152 insertedItem.InitializeDeferredLoaders();
156 /// <summary>
157 /// Clears out the foreign key values and parent object references for deleted objects on the child side of a relationship.
158 /// For bi-directional relationships, also performs the following fixup:
159 /// - for 1:N we remove the deleted entity from the opposite EntitySet or collection
160 /// - for 1:1 we null out the back reference
161 /// </summary>
162 private void ClearForeignKeyReferences(TrackedObject to) {
163 Debug.Assert(to.IsDeleted, "Foreign key reference cleanup should only happen on Deleted objects.");
164 foreach (MetaAssociation assoc in to.Type.Associations) {
165 if (assoc.IsForeignKey) {
166 // If there is a member on the other side referring back to us (i.e. this is a bi-directional relationship),
167 // we want to do a cache lookup to find the other side, then will remove ourselves from that collection.
168 // 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.
169 if (assoc.OtherMember != null && assoc.OtherKeyIsPrimaryKey) {
170 Debug.Assert(assoc.OtherMember.IsAssociation, "OtherMember of the association is expected to also be an association.");
171 // Search the cache for the target of the association, since
172 // it might not be loaded on the object being deleted, and we
173 // don't want to force a load.
174 object[] keyValues = CommonDataServices.GetForeignKeyValues(assoc, to.Current);
175 object cached = this.services.IdentityManager.Find(assoc.OtherType, keyValues);
177 if (cached != null) {
178 if (assoc.OtherMember.Association.IsMany) {
179 // Note that going through the IList interface handles
180 // EntitySet as well as POCO collections that implement IList
181 // and are not FixedSize.
182 System.Collections.IList collection = assoc.OtherMember.MemberAccessor.GetBoxedValue(cached) as System.Collections.IList;
183 if (collection != null && !collection.IsFixedSize) {
184 collection.Remove(to.Current);
185 // Explicitly clear the foreign key values and parent object reference
186 ClearForeignKeysHelper(assoc, to.Current);
189 else {
190 // Null out the other association. Since this is a 1:1 association,
191 // we're not concerned here with causing a deferred load, since the
192 // target is already cached (since we're deleting it).
193 assoc.OtherMember.MemberAccessor.SetBoxedValue(ref cached, null);
194 // Explicitly clear the foreign key values and parent object reference
195 ClearForeignKeysHelper(assoc, to.Current);
198 // else the item was not found in the cache, so there is no fixup that has to be done
199 // We are explicitly not calling ClearForeignKeysHelper because it breaks existing shipped behavior and we want to maintain backward compatibility
201 else {
202 // 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
203 ClearForeignKeysHelper(assoc, to.Current);
206 // else this is not the 1-side (foreign key) of the relationship, so there is nothing for us to do
210 // Ensure the the member and foreign keys are nulled so that after trackedInstance is deleted,
211 // the object does not appear to be associated with the other side anymore. This prevents the deleted object
212 // from referencing objects still in the cache, but also will prevent the related object from being implicitly loaded
213 private static void ClearForeignKeysHelper(MetaAssociation assoc, object trackedInstance) {
214 Debug.Assert(assoc.IsForeignKey, "Foreign key clearing should only happen on foreign key side of the association.");
215 Debug.Assert(assoc.ThisMember.IsAssociation, "Expected ThisMember of an association to always be an association.");
217 // If this member is one of our deferred loaders, and it does not already have a value, explicitly set the deferred source to
218 // null so that when we set the association member itself to null later, it doesn't trigger an implicit load.
219 // 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.
220 MetaDataMember thisMember = assoc.ThisMember;
222 if (thisMember.IsDeferred &&
223 !(thisMember.StorageAccessor.HasAssignedValue(trackedInstance) || thisMember.StorageAccessor.HasLoadedValue(trackedInstance)))
225 // If this is a deferred member, set the value directly in the deferred accessor instead of going
226 // through the normal member accessor, so that we don't trigger an implicit load.
227 thisMember.DeferredSourceAccessor.SetBoxedValue(ref trackedInstance, null);
230 // Notify the object that the relationship should be considered deleted.
231 // This allows the object to do its own fixup even when we can't do it automatically.
232 thisMember.MemberAccessor.SetBoxedValue(ref trackedInstance, null);
234 // Also set the foreign key values to null if possible
235 for (int i = 0, n = assoc.ThisKey.Count; i < n; i++) {
236 MetaDataMember thisKey = assoc.ThisKey[i];
237 if (thisKey.CanBeNull) {
238 thisKey.StorageAccessor.SetBoxedValue(ref trackedInstance, null);
243 private static void ValidateAll(IEnumerable<TrackedObject> list) {
244 foreach (var item in list) {
245 if (item.IsNew) {
246 item.SynchDependentData();
247 if (item.Type.HasAnyValidateMethod) {
248 SendOnValidate(item.Type, item, ChangeAction.Insert);
250 } else if (item.IsDeleted) {
251 if (item.Type.HasAnyValidateMethod) {
252 SendOnValidate(item.Type, item, ChangeAction.Delete);
254 } else if (item.IsPossiblyModified) {
255 item.SynchDependentData();
256 if (item.IsModified && item.Type.HasAnyValidateMethod) {
257 SendOnValidate(item.Type, item, ChangeAction.Update);
263 private static void SendOnValidate(MetaType type, TrackedObject item, ChangeAction changeAction) {
264 if (type != null) {
265 SendOnValidate(type.InheritanceBase, item, changeAction);
267 if (type.OnValidateMethod != null) {
268 try {
269 type.OnValidateMethod.Invoke(item.Current, new object[] { changeAction });
270 } catch (TargetInvocationException tie) {
271 if (tie.InnerException != null) {
272 throw tie.InnerException;
275 throw;
281 internal string GetChangeText() {
282 this.ObserveUntrackedObjects();
283 // Must apply inferred deletions only after any untracked objects
284 // are tracked
285 this.ApplyInferredDeletions();
286 this.BuildEdgeMaps();
288 // append change text only
289 StringBuilder changeText = new StringBuilder();
290 foreach (TrackedObject item in this.GetOrderedList()) {
291 if (item.IsNew) {
292 item.SynchDependentData();
293 changeDirector.AppendInsertText(item, changeText);
295 else if (item.IsDeleted) {
296 changeDirector.AppendDeleteText(item, changeText);
298 else if (item.IsPossiblyModified) {
299 item.SynchDependentData();
300 if (item.IsModified) {
301 changeDirector.AppendUpdateText(item, changeText);
305 return changeText.ToString();
308 internal ChangeSet GetChangeSet() {
309 List<object> newEntities = new List<object>();
310 List<object> deletedEntities = new List<object>();
311 List<object> changedEntities = new List<object>();
313 this.ObserveUntrackedObjects();
314 // Must apply inferred deletions only after any untracked objects
315 // are tracked
316 this.ApplyInferredDeletions();
318 foreach (TrackedObject item in this.tracker.GetInterestingObjects()) {
319 if (item.IsNew) {
320 item.SynchDependentData();
321 newEntities.Add(item.Current);
323 else if (item.IsDeleted) {
324 deletedEntities.Add(item.Current);
326 else if (item.IsPossiblyModified) {
327 item.SynchDependentData();
328 if (item.IsModified) {
329 changedEntities.Add(item.Current);
334 return new ChangeSet(newEntities.AsReadOnly(), deletedEntities.AsReadOnly(), changedEntities.AsReadOnly());
337 // verify that primary key and db-generated values have not changed
338 private static void CheckForInvalidChanges(TrackedObject tracked) {
339 foreach (MetaDataMember mem in tracked.Type.PersistentDataMembers) {
340 if (mem.IsPrimaryKey || mem.IsDbGenerated || mem.IsVersion) {
341 if (tracked.HasChangedValue(mem)) {
342 if (mem.IsPrimaryKey) {
343 throw Error.IdentityChangeNotAllowed(mem.Name, tracked.Type.Name);
345 else {
346 throw Error.DbGeneratedChangeNotAllowed(mem.Name, tracked.Type.Name);
353 /// <summary>
354 /// Create an ChangeConflictException with the best message
355 /// </summary>
356 static private ChangeConflictException CreateChangeConflictException(int totalUpdatesAttempted, int failedUpdates) {
357 string msg = Strings.RowNotFoundOrChanged;
358 if (totalUpdatesAttempted > 1) {
359 msg = Strings.UpdatesFailedMessage(failedUpdates, totalUpdatesAttempted);
361 return new ChangeConflictException(msg);
364 internal void TrackUntrackedObjects() {
365 Dictionary<object, object> visited = new Dictionary<object, object>();
367 // search for untracked new objects
368 List<TrackedObject> items = new List<TrackedObject>(this.tracker.GetInterestingObjects());
369 foreach (TrackedObject item in items) {
370 this.TrackUntrackedObjects(item.Type, item.Current, visited);
374 internal void ApplyInferredDeletions() {
375 foreach (TrackedObject item in this.tracker.GetInterestingObjects()) {
376 if (item.CanInferDelete()) {
377 // based on DeleteOnNull specifications on the item's associations,
378 // a deletion can be inferred for this item. The actual state transition
379 // is dependent on the current item state.
380 if (item.IsNew) {
381 item.ConvertToRemoved();
383 else if (item.IsPossiblyModified || item.IsModified) {
384 item.ConvertToDeleted();
390 private void TrackUntrackedObjects(MetaType type, object item, Dictionary<object, object> visited) {
391 if (!visited.ContainsKey(item)) {
392 visited.Add(item, item);
393 TrackedObject tracked = this.tracker.GetTrackedObject(item);
394 if (tracked == null) {
395 tracked = this.tracker.Track(item);
396 tracked.ConvertToNew();
398 else if (tracked.IsDead || tracked.IsRemoved) {
399 // ignore
400 return;
403 // search parents (objects we are dependent on)
404 foreach (RelatedItem parent in this.services.GetParents(type, item)) {
405 this.TrackUntrackedObjects(parent.Type, parent.Item, visited);
408 // synch up primary key
409 if (tracked.IsNew) {
410 tracked.InitializeDeferredLoaders();
412 if (!tracked.IsPendingGeneration(tracked.Type.IdentityMembers)) {
413 tracked.SynchDependentData();
414 object cached = this.services.InsertLookupCachedObject(tracked.Type, item);
415 if (cached != item) {
416 TrackedObject cachedTracked = this.tracker.GetTrackedObject(cached);
417 Debug.Assert(cachedTracked != null);
418 if (cachedTracked.IsDeleted || cachedTracked.CanInferDelete()) {
419 // adding new object with same ID as object being deleted.. turn into modified
420 tracked.ConvertToPossiblyModified(cachedTracked.Original);
421 // turn deleted to dead...
422 cachedTracked.ConvertToDead();
424 this.services.RemoveCachedObjectLike(tracked.Type, item);
425 this.services.InsertLookupCachedObject(tracked.Type, item);
427 else if (!cachedTracked.IsDead) {
428 throw new DuplicateKeyException(item, Strings.CantAddAlreadyExistingKey);
432 else {
433 // we may have a generated PK, however we set the PK on this new item to
434 // match a deleted item
435 object cached = this.services.GetCachedObjectLike(tracked.Type, item);
436 if (cached != null) {
437 TrackedObject cachedTracked = this.tracker.GetTrackedObject(cached);
438 Debug.Assert(cachedTracked != null);
439 if (cachedTracked.IsDeleted || cachedTracked.CanInferDelete()) {
440 // adding new object with same ID as object being deleted.. turn into modified
441 tracked.ConvertToPossiblyModified(cachedTracked.Original);
442 // turn deleted to dead...
443 cachedTracked.ConvertToDead();
445 this.services.RemoveCachedObjectLike(tracked.Type, item);
446 this.services.InsertLookupCachedObject(tracked.Type, item);
452 // search children (objects that are dependent on us)
453 foreach (RelatedItem child in this.services.GetChildren(type, item)) {
454 this.TrackUntrackedObjects(child.Type, child.Item, visited);
459 internal void ObserveUntrackedObjects() {
460 Dictionary<object, object> visited = new Dictionary<object, object>();
462 List<TrackedObject> items = new List<TrackedObject>(this.tracker.GetInterestingObjects());
463 foreach (TrackedObject item in items) {
464 this.ObserveUntrackedObjects(item.Type, item.Current, visited);
468 private void ObserveUntrackedObjects(MetaType type, object item, Dictionary<object, object> visited) {
469 if (!visited.ContainsKey(item)) {
470 visited.Add(item, item);
471 TrackedObject tracked = this.tracker.GetTrackedObject(item);
472 if (tracked == null) {
473 tracked = this.tracker.Track(item);
474 tracked.ConvertToNew();
475 } else if (tracked.IsDead || tracked.IsRemoved) {
476 // ignore
477 return;
480 // search parents (objects we are dependent on)
481 foreach (RelatedItem parent in this.services.GetParents(type, item)) {
482 this.ObserveUntrackedObjects(parent.Type, parent.Item, visited);
485 // synch up primary key unless its generated.
486 if (tracked.IsNew) {
487 if (!tracked.IsPendingGeneration(tracked.Type.IdentityMembers)) {
488 tracked.SynchDependentData();
492 // search children (objects that are dependent on us)
493 foreach (RelatedItem child in this.services.GetChildren(type, item)) {
494 this.ObserveUntrackedObjects(child.Type, child.Item, visited);
499 private TrackedObject GetOtherItem(MetaAssociation assoc, object instance) {
500 if (instance == null)
501 return null;
502 object other = null;
503 // Don't load unloaded references
504 if (assoc.ThisMember.StorageAccessor.HasAssignedValue(instance) ||
505 assoc.ThisMember.StorageAccessor.HasLoadedValue(instance)
507 other = assoc.ThisMember.MemberAccessor.GetBoxedValue(instance);
509 else if (assoc.OtherKeyIsPrimaryKey) {
510 // Maybe it's in the cache, but not yet attached through reference.
511 object[] foreignKeys = CommonDataServices.GetForeignKeyValues(assoc, instance);
512 other = this.services.GetCachedObject(assoc.OtherType, foreignKeys);
514 // else the other key is not the primary key so there is no way to try to look it up
515 return (other != null) ? this.tracker.GetTrackedObject(other) : null;
518 private bool HasAssociationChanged(MetaAssociation assoc, TrackedObject item) {
519 if (item.Original != null && item.Current != null) {
520 if (assoc.ThisMember.StorageAccessor.HasAssignedValue(item.Current) ||
521 assoc.ThisMember.StorageAccessor.HasLoadedValue(item.Current)
523 return this.GetOtherItem(assoc, item.Current) != this.GetOtherItem(assoc, item.Original);
525 else {
526 object[] currentFKs = CommonDataServices.GetForeignKeyValues(assoc, item.Current);
527 object[] originaFKs = CommonDataServices.GetForeignKeyValues(assoc, item.Original);
528 for (int i = 0, n = currentFKs.Length; i < n; i++) {
529 if (!object.Equals(currentFKs[i], originaFKs[i]))
530 return true;
534 return false;
537 private void BuildEdgeMaps() {
538 this.currentParentEdges.Clear();
539 this.originalChildEdges.Clear();
540 this.originalChildReferences.Clear();
542 List<TrackedObject> list = new List<TrackedObject>(this.tracker.GetInterestingObjects());
543 foreach (TrackedObject item in list) {
544 bool isNew = item.IsNew;
545 MetaType mt = item.Type;
546 foreach (MetaAssociation assoc in mt.Associations) {
547 if (assoc.IsForeignKey) {
548 TrackedObject otherItem = this.GetOtherItem(assoc, item.Current);
549 TrackedObject dbOtherItem = this.GetOtherItem(assoc, item.Original);
550 bool pointsToDeleted = (otherItem != null && otherItem.IsDeleted) || (dbOtherItem != null && dbOtherItem.IsDeleted);
551 bool pointsToNew = (otherItem != null && otherItem.IsNew);
553 if (isNew || pointsToDeleted || pointsToNew || this.HasAssociationChanged(assoc, item)) {
554 if (otherItem != null) {
555 this.currentParentEdges.Add(assoc, item, otherItem);
557 if (dbOtherItem != null) {
558 if (assoc.IsUnique) {
559 this.originalChildEdges.Add(assoc, dbOtherItem, item);
561 this.originalChildReferences.Add(dbOtherItem, item);
569 enum VisitState {
570 Before,
571 After
574 private List<TrackedObject> GetOrderedList() {
575 var objects = this.tracker.GetInterestingObjects().ToList();
577 // give list an initial order (most likely correct order) to avoid deadlocks in server
578 var range = Enumerable.Range(0, objects.Count).ToList();
579 range.Sort((int x, int y) => Compare(objects[x], x, objects[y], y));
580 var ordered = range.Select(i => objects[i]).ToList();
582 // permute order if constraint dependencies requires some changes to come before others
583 var visited = new Dictionary<TrackedObject, VisitState>();
584 var list = new List<TrackedObject>();
585 foreach (TrackedObject item in ordered) {
586 this.BuildDependencyOrderedList(item, list, visited);
588 return list;
591 private static int Compare(TrackedObject x, int xOrdinal, TrackedObject y, int yOrdinal) {
592 // deal with possible nulls
593 if (x == y) {
594 return 0;
596 if (x == null) {
597 return -1;
599 else if (y == null) {
600 return 1;
602 // first order by action: Inserts first, Updates, Deletes last
603 int xAction = x.IsNew ? 0 : x.IsDeleted ? 2 : 1;
604 int yAction = y.IsNew ? 0 : y.IsDeleted ? 2 : 1;
605 if (xAction < yAction) {
606 return -1;
608 else if (xAction > yAction) {
609 return 1;
611 // no need to order inserts (PK's may not even exist)
612 if (x.IsNew) {
613 // keep original order
614 return xOrdinal.CompareTo(yOrdinal);
616 // second order by type
617 if (x.Type != y.Type) {
618 return string.CompareOrdinal(x.Type.Type.FullName, y.Type.Type.FullName);
620 // lastly, order by PK values
621 int result = 0;
622 foreach (MetaDataMember mm in x.Type.IdentityMembers) {
623 object xValue = mm.StorageAccessor.GetBoxedValue(x.Current);
624 object yValue = mm.StorageAccessor.GetBoxedValue(y.Current);
625 if (xValue == null) {
626 if (yValue != null) {
627 return -1;
630 else {
631 IComparable xc = xValue as IComparable;
632 if (xc != null) {
633 result = xc.CompareTo(yValue);
634 if (result != 0) {
635 return result;
640 // they are the same? leave in original order
641 return xOrdinal.CompareTo(yOrdinal);
644 private void BuildDependencyOrderedList(TrackedObject item, List<TrackedObject> list, Dictionary<TrackedObject, VisitState> visited) {
645 VisitState state;
646 if (visited.TryGetValue(item, out state)) {
647 if (state == VisitState.Before) {
648 throw Error.CycleDetected();
650 return;
653 visited[item] = VisitState.Before;
655 if (item.IsInteresting) {
656 if (item.IsDeleted) {
657 // if 'item' is deleted
658 // all objects that used to refer to 'item' must be ordered before item
659 foreach (TrackedObject other in this.originalChildReferences[item]) {
660 if (other != item) {
661 this.BuildDependencyOrderedList(other, list, visited);
665 else {
666 // if 'item' is new or changed
667 // for all objects 'other' that 'item' refers to along association 'assoc'
668 // if 'other' is new then 'other' must be ordered before 'item'
669 // if 'assoc' is pure one-to-one and some other item 'prevItem' used to refer to 'other'
670 // then 'prevItem' must be ordered before 'item'
671 foreach (MetaAssociation assoc in item.Type.Associations) {
672 if (assoc.IsForeignKey) {
673 TrackedObject other = this.currentParentEdges[assoc, item];
674 if (other != null) {
675 if (other.IsNew) {
676 // if other is new, visit other first (since item's FK depends on it)
677 if (other != item || item.Type.DBGeneratedIdentityMember != null) {
678 this.BuildDependencyOrderedList(other, list, visited);
681 else if ((assoc.IsUnique || assoc.ThisKeyIsPrimaryKey)) {
682 TrackedObject prevItem = this.originalChildEdges[assoc, other];
683 if (prevItem != null && other != item) {
684 this.BuildDependencyOrderedList(prevItem, list, visited);
692 list.Add(item);
695 visited[item] = VisitState.After;
698 class EdgeMap {
699 Dictionary<MetaAssociation, Dictionary<TrackedObject, TrackedObject>> associations;
701 internal EdgeMap() {
702 this.associations = new Dictionary<MetaAssociation, Dictionary<TrackedObject, TrackedObject>>();
705 internal void Add(MetaAssociation assoc, TrackedObject from, TrackedObject to) {
706 Dictionary<TrackedObject, TrackedObject> pairs;
707 if (!associations.TryGetValue(assoc, out pairs)) {
708 pairs = new Dictionary<TrackedObject, TrackedObject>();
709 associations.Add(assoc, pairs);
711 pairs.Add(from, to);
714 internal TrackedObject this[MetaAssociation assoc, TrackedObject from] {
715 get {
716 Dictionary<TrackedObject, TrackedObject> pairs;
717 if (associations.TryGetValue(assoc, out pairs)) {
718 TrackedObject to;
719 if (pairs.TryGetValue(from, out to)) {
720 return to;
723 return null;
726 internal void Clear() {
727 this.associations.Clear();
731 class ReferenceMap {
732 Dictionary<TrackedObject, List<TrackedObject>> references;
734 internal ReferenceMap() {
735 this.references = new Dictionary<TrackedObject, List<TrackedObject>>();
738 internal void Add(TrackedObject from, TrackedObject to) {
739 List<TrackedObject> refs;
740 if (!references.TryGetValue(from, out refs)) {
741 refs = new List<TrackedObject>();
742 references.Add(from, refs);
744 if (!refs.Contains(to))
745 refs.Add(to);
748 internal IEnumerable<TrackedObject> this[TrackedObject from] {
749 get {
750 List<TrackedObject> refs;
751 if (references.TryGetValue(from, out refs)) {
752 return refs;
754 return Empty;
758 internal void Clear() {
759 this.references.Clear();
762 private static TrackedObject[] Empty = new TrackedObject[] { };