2016-08-03 10:59:49 +00:00
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 ;
/// <summary>
/// Describes the type of change the entity will undergo when submitted to the database.
/// </summary>
public enum ChangeAction {
/// <summary>
/// The entity will not be submitted.
/// </summary>
None = 0 ,
/// <summary>
/// The entity will be deleted.
/// </summary>
Delete ,
/// <summary>
/// The entity will be inserted.
/// </summary>
Insert ,
/// <summary>
/// The entity will be updated.
/// </summary>
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 < ObjectChangeConflict > conflicts = new List < ObjectChangeConflict > ( ) ;
List < TrackedObject > deletedItems = new List < TrackedObject > ( ) ;
List < TrackedObject > insertedItems = new List < TrackedObject > ( ) ;
List < TrackedObject > syncDependentItems = new List < TrackedObject > ( ) ;
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 ) {
2017-08-21 15:34:15 +00:00
// First we need to rollback any value that have already been auto-sync'd, since the values are no longer valid on the server
2016-08-03 10:59:49 +00:00
changeDirector . RollbackAutoSync ( ) ;
2017-08-21 15:34:15 +00:00
// Also rollback any dependent items that were sync'd, since their parent values may have been rolled back
2016-08-03 10:59:49 +00:00
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 < TrackedObject > insertedItems , List < TrackedObject > 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 ( ) ;
}
}
/// <summary>
/// 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
/// </summary>
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 < TrackedObject > 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 < object > newEntities = new List < object > ( ) ;
List < object > deletedEntities = new List < object > ( ) ;
List < object > changedEntities = new List < object > ( ) ;
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 ) ;
}
}
}
}
}
/// <summary>
/// Create an ChangeConflictException with the best message
/// </summary>
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 < object , object > visited = new Dictionary < object , object > ( ) ;
// search for untracked new objects
List < TrackedObject > items = new List < TrackedObject > ( 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 < object , object > 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 < object , object > visited = new Dictionary < object , object > ( ) ;
List < TrackedObject > items = new List < TrackedObject > ( this . tracker . GetInterestingObjects ( ) ) ;
foreach ( TrackedObject item in items ) {
this . ObserveUntrackedObjects ( item . Type , item . Current , visited ) ;
}
}
private void ObserveUntrackedObjects ( MetaType type , object item , Dictionary < object , object > 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 < TrackedObject > list = new List < TrackedObject > ( 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 < TrackedObject > 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 < TrackedObject , VisitState > ( ) ;
var list = new List < TrackedObject > ( ) ;
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 < TrackedObject > list , Dictionary < TrackedObject , VisitState > 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 < MetaAssociation , Dictionary < TrackedObject , TrackedObject > > associations ;
internal EdgeMap ( ) {
this . associations = new Dictionary < MetaAssociation , Dictionary < TrackedObject , TrackedObject > > ( ) ;
}
internal void Add ( MetaAssociation assoc , TrackedObject from , TrackedObject to ) {
Dictionary < TrackedObject , TrackedObject > pairs ;
if ( ! associations . TryGetValue ( assoc , out pairs ) ) {
pairs = new Dictionary < TrackedObject , TrackedObject > ( ) ;
associations . Add ( assoc , pairs ) ;
}
pairs . Add ( from , to ) ;
}
internal TrackedObject this [ MetaAssociation assoc , TrackedObject from ] {
get {
Dictionary < TrackedObject , TrackedObject > 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 < TrackedObject , List < TrackedObject > > references ;
internal ReferenceMap ( ) {
this . references = new Dictionary < TrackedObject , List < TrackedObject > > ( ) ;
}
internal void Add ( TrackedObject from , TrackedObject to ) {
List < TrackedObject > refs ;
if ( ! references . TryGetValue ( from , out refs ) ) {
refs = new List < TrackedObject > ( ) ;
references . Add ( from , refs ) ;
}
if ( ! refs . Contains ( to ) )
refs . Add ( to ) ;
}
internal IEnumerable < TrackedObject > this [ TrackedObject from ] {
get {
List < TrackedObject > refs ;
if ( references . TryGetValue ( from , out refs ) ) {
return refs ;
}
return Empty ;
}
}
internal void Clear ( ) {
this . references . Clear ( ) ;
}
private static TrackedObject [ ] Empty = new TrackedObject [ ] { } ;
}
}
}