diff --git a/src/ChangeManagement/ChangeTracker.cs b/src/ChangeManagement/ChangeTracker.cs index 78636dc..f8b3fc6 100644 --- a/src/ChangeManagement/ChangeTracker.cs +++ b/src/ChangeManagement/ChangeTracker.cs @@ -39,24 +39,6 @@ internal abstract class ChangeTracker internal abstract void AcceptChanges(); internal abstract IEnumerable GetInterestingObjects(); - - #region Private classes - /// - /// This is the implementation used when change tracking is disabled. - /// - private class ReadOnlyChangeTracker : ChangeTracker - { - internal override TrackedObject Track(object obj) { return null; } - internal override TrackedObject Track(object obj, bool recurse) { return null; } - internal override void FastTrack(object obj) { } - internal override bool IsTracked(object obj) { return false; } - internal override TrackedObject GetTrackedObject(object obj) { return null; } - internal override void StopTracking(object obj) { } - internal override void AcceptChanges() { } - internal override IEnumerable GetInterestingObjects() { return new TrackedObject[0]; } - } - #endregion - internal static ChangeTracker CreateChangeTracker(CommonDataServices dataServices, bool asReadOnly) { if(asReadOnly) @@ -69,981 +51,5 @@ internal static ChangeTracker CreateChangeTracker(CommonDataServices dataService } } - class StandardChangeTracker : ChangeTracker - { - Dictionary items; - PropertyChangingEventHandler onPropertyChanging; - CommonDataServices services; - - internal StandardChangeTracker(CommonDataServices services) - { - this.services = services; - this.items = new Dictionary(); - this.onPropertyChanging = new PropertyChangingEventHandler(this.OnPropertyChanging); - } - - /// - /// Given a type root and a discriminator, return the type that would be instantiated. - /// - private static MetaType TypeFromDiscriminator(MetaType root, object discriminator) - { - foreach(MetaType type in root.InheritanceTypes) - { - if(IsSameDiscriminator(discriminator, type.InheritanceCode)) - return type; - } - return root.InheritanceDefault; - } - - private static bool IsSameDiscriminator(object discriminator1, object discriminator2) - { - if(discriminator1 == discriminator2) - { - return true; - } - if(discriminator1 == null || discriminator2 == null) - { - return false; - } - return discriminator1.Equals(discriminator2); - } - - internal override TrackedObject Track(object obj) - { - return Track(obj, false); - } - - internal override TrackedObject Track(object obj, bool recurse) - { - MetaType type = this.services.Model.GetMetaType(obj.GetType()); - Dictionary visited = new Dictionary(); - return Track(type, obj, visited, recurse, 1); - } - - private TrackedObject Track(MetaType mt, object obj, Dictionary visited, bool recurse, int level) - { - StandardTrackedObject tracked = (StandardTrackedObject)this.GetTrackedObject(obj); - if(tracked != null || visited.ContainsKey(obj)) - { - return tracked; - } - - // The root object tracked is tracked normally - all other objects - // in the reference graph are weakly tracked. - bool weaklyTrack = level > 1; - tracked = new StandardTrackedObject(this, mt, obj, obj, weaklyTrack); - if(tracked.HasDeferredLoaders) - { - throw Error.CannotAttachAddNonNewEntities(); - } - this.items.Add(obj, tracked); - this.Attach(obj); - visited.Add(obj, obj); - - if(recurse) - { - // track parents (objects we are dependent on) - foreach(RelatedItem parent in this.services.GetParents(mt, obj)) - { - this.Track(parent.Type, parent.Item, visited, recurse, level + 1); - } - - // track children (objects that are dependent on us) - foreach(RelatedItem child in this.services.GetChildren(mt, obj)) - { - this.Track(child.Type, child.Item, visited, recurse, level + 1); - } - } - - return tracked; - } - - internal override void FastTrack(object obj) - { - // assumes object is already in identity cache - this.Attach(obj); - } - - internal override void StopTracking(object obj) - { - this.Detach(obj); - this.items.Remove(obj); - } - - internal override bool IsTracked(object obj) - { - return this.items.ContainsKey(obj) || this.IsFastTracked(obj); - } - - private bool IsFastTracked(object obj) - { - MetaType type = this.services.Model.GetTable(obj.GetType()).RowType; - return this.services.IsCachedObject(type, obj); - } - - internal override TrackedObject GetTrackedObject(object obj) - { - StandardTrackedObject ti; - if(!this.items.TryGetValue(obj, out ti)) - { - if(this.IsFastTracked(obj)) - { - return this.PromoteFastTrackedObject(obj); - } - } - return ti; - } - - private StandardTrackedObject PromoteFastTrackedObject(object obj) - { - Type type = obj.GetType(); - MetaType metaType = this.services.Model.GetTable(type).RowType.GetInheritanceType(type); - return this.PromoteFastTrackedObject(metaType, obj); - } - - private StandardTrackedObject PromoteFastTrackedObject(MetaType type, object obj) - { - StandardTrackedObject ti = new StandardTrackedObject(this, type, obj, obj); - this.items.Add(obj, ti); - return ti; - } - - private void Attach(object obj) - { - INotifyPropertyChanging notifier = obj as INotifyPropertyChanging; - if(notifier != null) - { - notifier.PropertyChanging += this.onPropertyChanging; - } - else - { - // if has no notifier, consider it modified already - this.OnPropertyChanging(obj, null); - } - } - - private void Detach(object obj) - { - INotifyPropertyChanging notifier = obj as INotifyPropertyChanging; - if(notifier != null) - { - notifier.PropertyChanging -= this.onPropertyChanging; - } - } - - private void OnPropertyChanging(object sender, PropertyChangingEventArgs args) - { - StandardTrackedObject ti; - if(this.items.TryGetValue(sender, out ti)) - { - ti.StartTracking(); - } - else if(this.IsFastTracked(sender)) - { - ti = this.PromoteFastTrackedObject(sender); - ti.StartTracking(); - } - } - - internal override void AcceptChanges() - { - List list = new List((IEnumerable)this.items.Values); - foreach(TrackedObject item in list) - { - item.AcceptChanges(); - } - } - - internal override IEnumerable GetInterestingObjects() - { - foreach(StandardTrackedObject ti in this.items.Values) - { - if(ti.IsInteresting) - { - yield return ti; - } - } - } - - class StandardTrackedObject : TrackedObject - { - private StandardChangeTracker tracker; - private MetaType type; - private object current; - private object original; - private State state; - private BitArray dirtyMemberCache; - private bool haveInitializedDeferredLoaders; - private bool isWeaklyTracked; - - enum State - { - New, - Deleted, - PossiblyModified, - Modified, - Removed, - Dead - } - - public override string ToString() - { - return type.Name + ":" + GetState(); - } - - private string GetState() - { - switch(this.state) - { - case State.New: - case State.Deleted: - case State.Dead: - case State.Removed: - return this.state.ToString(); - default: - if(this.IsModified) - { - return "Modified"; - } - else - { - return "Unmodified"; - } - } - } - - internal StandardTrackedObject(StandardChangeTracker tracker, MetaType type, object current, object original) - { - if(current == null) - { - throw Error.ArgumentNull("current"); - } - this.tracker = tracker; - this.type = type.GetInheritanceType(current.GetType()); - this.current = current; - this.original = original; - this.state = State.PossiblyModified; - dirtyMemberCache = new BitArray(this.type.DataMembers.Count); - } - - internal StandardTrackedObject(StandardChangeTracker tracker, MetaType type, object current, object original, bool isWeaklyTracked) - : this(tracker, type, current, original) - { - this.isWeaklyTracked = isWeaklyTracked; - } - - internal override bool IsWeaklyTracked - { - get { return isWeaklyTracked; } - } - - internal override MetaType Type - { - get { return this.type; } - } - - internal override object Current - { - get { return this.current; } - } - - internal override object Original - { - get { return this.original; } - } - - internal override bool IsNew - { - get { return this.state == State.New; } - } - - internal override bool IsDeleted - { - get { return this.state == State.Deleted; } - } - - internal override bool IsRemoved - { - get { return this.state == State.Removed; } - } - - internal override bool IsDead - { - get { return this.state == State.Dead; } - } - - internal override bool IsModified - { - get { return this.state == State.Modified || (this.state == State.PossiblyModified && this.current != this.original && this.HasChangedValues()); } - } - - internal override bool IsUnmodified - { - get { return this.state == State.PossiblyModified && (this.current == this.original || !this.HasChangedValues()); } - } - - internal override bool IsPossiblyModified - { - get { return this.state == State.Modified || this.state == State.PossiblyModified; } - } - - internal override bool CanInferDelete() - { - // A delete can be inferred iff there is a non-nullable singleton association that has - // been set to null, and the association has DeleteOnNull = true. - if(this.state == State.Modified || this.state == State.PossiblyModified) - { - foreach(MetaAssociation assoc in Type.Associations) - { - if(assoc.DeleteOnNull && assoc.IsForeignKey && !assoc.IsNullable && !assoc.IsMany && - assoc.ThisMember.StorageAccessor.HasAssignedValue(Current) && - assoc.ThisMember.StorageAccessor.GetBoxedValue(Current) == null) - { - return true; - } - } - } - return false; - } - - internal override bool IsInteresting - { - get - { - return this.state == State.New || - this.state == State.Deleted || - this.state == State.Modified || - (this.state == State.PossiblyModified && this.current != this.original) || - CanInferDelete(); - } - } - - internal override void ConvertToNew() - { - // must be new or unmodified or removed to convert to new - System.Diagnostics.Debug.Assert(this.IsNew || this.IsRemoved || this.IsUnmodified); - this.original = null; - this.state = State.New; - } - - internal override void ConvertToPossiblyModified() - { - System.Diagnostics.Debug.Assert(this.IsPossiblyModified || this.IsDeleted); - this.state = State.PossiblyModified; - this.isWeaklyTracked = false; - } - - internal override void ConvertToModified() - { - System.Diagnostics.Debug.Assert(this.IsPossiblyModified); - System.Diagnostics.Debug.Assert(this.type.VersionMember != null || !this.type.HasUpdateCheck); - this.state = State.Modified; - this.isWeaklyTracked = false; - } - - internal override void ConvertToPossiblyModified(object originalState) - { - // must be modified or unmodified to convert to modified - System.Diagnostics.Debug.Assert(this.IsNew || this.IsPossiblyModified); - System.Diagnostics.Debug.Assert(originalState != null); - System.Diagnostics.Debug.Assert(originalState.GetType() == this.type.Type); - this.state = State.PossiblyModified; - this.original = this.CreateDataCopy(originalState); - this.isWeaklyTracked = false; - } - - internal override void ConvertToDeleted() - { - // must be modified or unmodified to be deleted - System.Diagnostics.Debug.Assert(this.IsDeleted || this.IsPossiblyModified); - this.state = State.Deleted; - this.isWeaklyTracked = false; - } - - internal override void ConvertToDead() - { - System.Diagnostics.Debug.Assert(this.IsDead || this.IsDeleted); - this.state = State.Dead; - this.isWeaklyTracked = false; - } - - internal override void ConvertToRemoved() - { - System.Diagnostics.Debug.Assert(this.IsRemoved || this.IsNew); - this.state = State.Removed; - this.isWeaklyTracked = false; - } - - internal override void ConvertToUnmodified() - { - System.Diagnostics.Debug.Assert(this.IsNew || this.IsPossiblyModified); - // reset to unmodified - this.state = State.PossiblyModified; - if(this.current is INotifyPropertyChanging) - { - this.original = this.current; - } - else - { - this.original = this.CreateDataCopy(this.current); - } - this.ResetDirtyMemberTracking(); - this.isWeaklyTracked = false; - } - - internal override void AcceptChanges() - { - if(IsWeaklyTracked) - { - InitializeDeferredLoaders(); - isWeaklyTracked = false; - } - if(this.IsDeleted) - { - this.ConvertToDead(); - } - else if(this.IsNew) - { - this.InitializeDeferredLoaders(); - this.ConvertToUnmodified(); - } - else if(this.IsPossiblyModified) - { - this.ConvertToUnmodified(); - } - } - - private void AssignMember(object instance, MetaDataMember mm, object value) - { - // In the unnotified case, directly use the storage accessor - // for everything because there are not events to be fired. - if(!(this.current is INotifyPropertyChanging)) - { - mm.StorageAccessor.SetBoxedValue(ref instance, value); - } - else - { - // Go through the member accessor to fire events. - mm.MemberAccessor.SetBoxedValue(ref instance, value); - } - } - - /// - /// Certain state is saved during change tracking to enable modifications - /// to be detected taking refresh operations into account. When changes - /// are reverted or accepted, this state must be reset. - /// - private void ResetDirtyMemberTracking() - { - this.dirtyMemberCache.SetAll(false); - } - - /// - /// Refresh internal tracking state using the original value and mode - /// specified. - /// - internal override void Refresh(RefreshMode mode, object freshInstance) - { - this.SynchDependentData(); - - // This must be done prior to updating original values - this.UpdateDirtyMemberCache(); - - // Apply the refresh strategy to each data member - Type instanceType = freshInstance.GetType(); - foreach(MetaDataMember mm in type.PersistentDataMembers) - { - var memberMode = mm.IsDbGenerated ? RefreshMode.OverwriteCurrentValues : mode; - if(memberMode != RefreshMode.KeepCurrentValues) - { - if(!mm.IsAssociation && (this.Type.Type == instanceType || mm.DeclaringType.Type.IsAssignableFrom(instanceType))) - { - object freshValue = mm.StorageAccessor.GetBoxedValue(freshInstance); - this.RefreshMember(mm, memberMode, freshValue); - } - } - } - - // Make the new data the current original value - this.original = this.CreateDataCopy(freshInstance); - - if(mode == RefreshMode.OverwriteCurrentValues) - { - this.ResetDirtyMemberTracking(); - } - } - - /// - /// Using the last saved comparison baseline, figure out which members have - /// changed since the last refresh, and save that information. This must be - /// done BEFORE any merge operations modify the current values. - /// - private void UpdateDirtyMemberCache() - { - // iterate over all members, and if they differ from - // last read values, mark as dirty - foreach(MetaDataMember mm in type.PersistentDataMembers) - { - if(mm.IsAssociation && mm.Association.IsMany) - { - continue; - } - if(!this.dirtyMemberCache.Get(mm.Ordinal) && this.HasChangedValue(mm)) - { - this.dirtyMemberCache.Set(mm.Ordinal, true); - } - } - } - - internal override void RefreshMember(MetaDataMember mm, RefreshMode mode, object freshValue) - { - System.Diagnostics.Debug.Assert(!mm.IsAssociation); - - if(mode == RefreshMode.KeepCurrentValues) - { - return; - } - - bool hasUserChange = this.HasChangedValue(mm); - - // we don't want to overwrite any modified values, unless - // the mode is original wins - if(hasUserChange && mode != RefreshMode.OverwriteCurrentValues) - return; - - object currentValue = mm.StorageAccessor.GetBoxedValue(this.current); - if(!object.Equals(freshValue, currentValue)) - { - mm.StorageAccessor.SetBoxedValue(ref this.current, freshValue); - - // update all singleton associations that are affected by a change to this member - foreach(MetaDataMember am in this.GetAssociationsForKey(mm)) - { - if(!am.Association.IsMany) - { - IEnumerable ds = this.tracker.services.GetDeferredSourceFactory(am).CreateDeferredSource(this.current); - if(am.StorageAccessor.HasValue(this.current)) - { - this.AssignMember(this.current, am, ds.Cast().SingleOrDefault()); - } - } - } - } - } - - private IEnumerable GetAssociationsForKey(MetaDataMember key) - { - foreach(MetaDataMember mm in this.type.PersistentDataMembers) - { - if(mm.IsAssociation && mm.Association.ThisKey.Contains(key)) - { - yield return mm; - } - } - } - - internal override object CreateDataCopy(object instance) - { - System.Diagnostics.Debug.Assert(instance != null); - Type instanceType = instance.GetType(); - System.Diagnostics.Debug.Assert(instance.GetType() == this.type.Type); - - object copy = Activator.CreateInstance(this.Type.Type); - - MetaType rootMetaType = this.tracker.services.Model.GetTable(instanceType).RowType.InheritanceRoot; - foreach(MetaDataMember mm in rootMetaType.GetInheritanceType(instanceType).PersistentDataMembers) - { - if(this.Type.Type != instanceType && !mm.DeclaringType.Type.IsAssignableFrom(instanceType)) - { - continue; - } - if(mm.IsDeferred) - { - // do not copy associations - if(!mm.IsAssociation) - { - if(mm.StorageAccessor.HasValue(instance)) - { - object value = mm.DeferredValueAccessor.GetBoxedValue(instance); - mm.DeferredValueAccessor.SetBoxedValue(ref copy, value); - } - else - { - IEnumerable ds = this.tracker.services.GetDeferredSourceFactory(mm).CreateDeferredSource(copy); - mm.DeferredSourceAccessor.SetBoxedValue(ref copy, ds); - } - } - } - else - { - // otherwise assign the value as-is to the backup instance - object value = mm.StorageAccessor.GetBoxedValue(instance); - // assumes member values are immutable or will communicate changes to entity - // note: byte[] and char[] don't do this. - mm.StorageAccessor.SetBoxedValue(ref copy, value); - } - } - return copy; - } - - internal void StartTracking() - { - if(this.original == this.current) - { - this.original = this.CreateDataCopy(this.current); - } - } - - // Return value indicates whether or not any data was actually [....]'d - internal override bool SynchDependentData() - { - bool valueWasSet = false; - - // set foreign key fields - foreach(MetaAssociation assoc in this.Type.Associations) - { - MetaDataMember mm = assoc.ThisMember; - if(assoc.IsForeignKey) - { - bool hasAssigned = mm.StorageAccessor.HasAssignedValue(this.current); - bool hasLoaded = mm.StorageAccessor.HasLoadedValue(this.current); - if(hasAssigned || hasLoaded) - { - object parent = mm.StorageAccessor.GetBoxedValue(this.current); - if(parent != null) - { - // copy parent's current primary key into this instance's foreign key fields - for(int i = 0, n = assoc.ThisKey.Count; i < n; i++) - { - MetaDataMember accThis = assoc.ThisKey[i]; - MetaDataMember accParent = assoc.OtherKey[i]; - object parentValue = accParent.StorageAccessor.GetBoxedValue(parent); - accThis.StorageAccessor.SetBoxedValue(ref this.current, parentValue); - valueWasSet = true; - } - } - else if(assoc.IsNullable) - { - if(mm.IsDeferred || (this.original != null && mm.MemberAccessor.GetBoxedValue(this.original) != null)) - { - // no known parent? set to null - for(int i = 0, n = assoc.ThisKey.Count; i < n; i++) - { - MetaDataMember accThis = assoc.ThisKey[i]; - if(accThis.CanBeNull) - { - if(this.original != null && this.HasChangedValue(accThis)) - { - if(accThis.StorageAccessor.GetBoxedValue(this.current) != null) - { - throw Error.InconsistentAssociationAndKeyChange(accThis.Member.Name, mm.Member.Name); - } - } - else - { - accThis.StorageAccessor.SetBoxedValue(ref this.current, null); - valueWasSet = true; - } - } - } - } - } - else if(!hasLoaded) - { - //Else the parent association has been set to null; but the ID is not nullable so - //the value can not be set - StringBuilder keys = new StringBuilder(); - foreach(MetaDataMember key in assoc.ThisKey) - { - if(keys.Length > 0) - { - keys.Append(", "); - } - keys.AppendFormat("{0}.{1}", this.Type.Name.ToString(), key.Name); - } - throw Error.CouldNotRemoveRelationshipBecauseOneSideCannotBeNull(assoc.OtherType.Name, this.Type.Name, keys); - } - } - } - } - - /// Explicitly set any inheritance discriminator for item. - if(this.type.HasInheritance) - { - if(this.original != null) - { - object currentDiscriminator = type.Discriminator.MemberAccessor.GetBoxedValue(this.current); - MetaType currentTypeFromDiscriminator = TypeFromDiscriminator(this.type, currentDiscriminator); - object dbDiscriminator = type.Discriminator.MemberAccessor.GetBoxedValue(this.original); - MetaType dbTypeFromDiscriminator = TypeFromDiscriminator(this.type, dbDiscriminator); - - // Would the discriminator change also change the type? If so, its not allowed. - if(currentTypeFromDiscriminator != dbTypeFromDiscriminator) - { - throw Error.CannotChangeInheritanceType(dbDiscriminator, - currentDiscriminator, original.GetType().Name, currentTypeFromDiscriminator); - } - } - else - { - // No db value means this is an 'Add'. Set the discriminator. - MetaType currentType = type.GetInheritanceType(this.current.GetType()); - if(currentType.HasInheritanceCode) - { - object code = currentType.InheritanceCode; - this.type.Discriminator.MemberAccessor.SetBoxedValue(ref current, code); - valueWasSet = true; - } - } - } - return valueWasSet; - } - - internal override bool HasChangedValue(MetaDataMember mm) - { - if(this.current == this.original) - { - return false; - } - if(mm.IsAssociation && mm.Association.IsMany) - { - return mm.StorageAccessor.HasAssignedValue(this.original); - } - if(mm.StorageAccessor.HasValue(this.current)) - { - if(this.original != null && mm.StorageAccessor.HasValue(this.original)) - { - // If the member has ever been in a modified state - // in the past, it is considered modified - if(dirtyMemberCache.Get(mm.Ordinal)) - { - return true; - } - object baseline = mm.MemberAccessor.GetBoxedValue(this.original); - object currentValue = mm.MemberAccessor.GetBoxedValue(this.current); - if(!object.Equals(currentValue, baseline)) - { - return true; - } - return false; - } - else if(mm.IsDeferred && mm.StorageAccessor.HasAssignedValue(this.current)) - { - return true; - } - } - return false; - } - - internal override bool HasChangedValues() - { - if(this.current == this.original) - { - return false; - } - if(this.IsNew) - { - return true; - } - foreach(MetaDataMember mm in this.type.PersistentDataMembers) - { - if(!mm.IsAssociation && this.HasChangedValue(mm)) - { - return true; - } - } - return false; - } - - internal override IEnumerable GetModifiedMembers() - { - foreach(MetaDataMember mm in this.type.PersistentDataMembers) - { - if(this.IsModifiedMember(mm)) - { - object currentValue = mm.MemberAccessor.GetBoxedValue(this.current); - if(this.original != null && mm.StorageAccessor.HasValue(this.original)) - { - object originalValue = mm.MemberAccessor.GetBoxedValue(this.original); - yield return new ModifiedMemberInfo(mm.Member, currentValue, originalValue); - } - else if(this.original == null || (mm.IsDeferred && !mm.StorageAccessor.HasLoadedValue(this.current))) - { - yield return new ModifiedMemberInfo(mm.Member, currentValue, null); - } - } - } - } - - private bool IsModifiedMember(MetaDataMember member) - { - return !member.IsAssociation && - !member.IsPrimaryKey && - !member.IsVersion && - !member.IsDbGenerated && - member.StorageAccessor.HasAssignedValue(this.current) && - (this.state == State.Modified || - (this.state == State.PossiblyModified && this.HasChangedValue(member))); - } - - internal override bool HasDeferredLoaders - { - get - { - foreach(MetaAssociation assoc in this.Type.Associations) - { - if(HasDeferredLoader(assoc.ThisMember)) - { - return true; - } - } - IEnumerable deferredMembers = this.Type.PersistentDataMembers.Where(p => p.IsDeferred && !p.IsAssociation); - foreach(MetaDataMember deferredMember in deferredMembers) - { - if(HasDeferredLoader(deferredMember)) - { - return true; - } - } - return false; - } - } - - private bool HasDeferredLoader(MetaDataMember deferredMember) - { - if(!deferredMember.IsDeferred) - { - return false; - } - - MetaAccessor acc = deferredMember.StorageAccessor; - if(acc.HasAssignedValue(this.current) || acc.HasLoadedValue(this.current)) - { - return false; - } - MetaAccessor dsacc = deferredMember.DeferredSourceAccessor; - IEnumerable loader = (IEnumerable)dsacc.GetBoxedValue(this.current); - - return loader != null; - } - - /// - /// Called to initialize deferred loaders for New or Attached entities. - /// - internal override void InitializeDeferredLoaders() - { - if(this.tracker.services.Context.DeferredLoadingEnabled) - { - foreach(MetaAssociation assoc in this.Type.Associations) - { - // don't set loader on association that is dependent on unrealized generated values - if(!this.IsPendingGeneration(assoc.ThisKey)) - { - InitializeDeferredLoader(assoc.ThisMember); - } - } - IEnumerable deferredMembers = this.Type.PersistentDataMembers.Where(p => p.IsDeferred && !p.IsAssociation); - foreach(MetaDataMember deferredMember in deferredMembers) - { - // don't set loader on member that is dependent on unrealized generated values - if(!this.IsPendingGeneration(Type.IdentityMembers)) - { - InitializeDeferredLoader(deferredMember); - } - } - haveInitializedDeferredLoaders = true; - } - } - - private void InitializeDeferredLoader(MetaDataMember deferredMember) - { - MetaAccessor acc = deferredMember.StorageAccessor; - if(!acc.HasAssignedValue(this.current) && !acc.HasLoadedValue(this.current)) - { - MetaAccessor dsacc = deferredMember.DeferredSourceAccessor; - IEnumerable loader = (IEnumerable)dsacc.GetBoxedValue(this.current); - // don't reset loader on any deferred member that already has one - if(loader == null) - { - IDeferredSourceFactory factory = this.tracker.services.GetDeferredSourceFactory(deferredMember); - loader = factory.CreateDeferredSource(this.current); - dsacc.SetBoxedValue(ref this.current, loader); - - } - else if(loader != null && !haveInitializedDeferredLoaders) - { - // If loader is present but wasn't generated by us, then - // an attempt to Attach or Add an entity from another context - // has been made, which is not supported. - throw Error.CannotAttachAddNonNewEntities(); - } - } - } - - internal override bool IsPendingGeneration(IEnumerable key) - { - if(this.IsNew) - { - foreach(MetaDataMember member in key) - { - if(IsMemberPendingGeneration(member)) - { - return true; - } - } - } - return false; - } - - internal override bool IsMemberPendingGeneration(MetaDataMember keyMember) - { - if(this.IsNew && keyMember.IsDbGenerated) - { - return true; - } - // look for any FK association that has this key member (should only be one) - foreach(MetaAssociation assoc in type.Associations) - { - if(assoc.IsForeignKey) - { - int index = assoc.ThisKey.IndexOf(keyMember); - if(index > -1) - { - // we must have a reference to this other object to know if its side of - // the association is generated or not - object otherItem = null; - if(assoc.ThisMember.IsDeferred) - { - otherItem = assoc.ThisMember.DeferredValueAccessor.GetBoxedValue(this.current); - } - else - { - otherItem = assoc.ThisMember.StorageAccessor.GetBoxedValue(this.current); - } - if(otherItem != null) - { - if(assoc.IsMany) - { - // Can't be pending generation for a value that would have to be the same - // across many rows. - continue; - } - else - { - StandardTrackedObject trackedOther = (StandardTrackedObject)this.tracker.GetTrackedObject(otherItem); - if(trackedOther != null) - { - MetaDataMember otherMember = assoc.OtherKey[index]; - return trackedOther.IsMemberPendingGeneration(otherMember); - } - } - } - } - } - } - return false; - } - } - } } } \ No newline at end of file diff --git a/src/ChangeManagement/ReadOnlyChangeTracker.cs b/src/ChangeManagement/ReadOnlyChangeTracker.cs new file mode 100644 index 0000000..6f884cb --- /dev/null +++ b/src/ChangeManagement/ReadOnlyChangeTracker.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace System.Data.Linq +{ + using System.Data.Linq.Mapping; + using System.Data.Linq.Provider; + + /// + /// This is the implementation used when change tracking is disabled. + /// + internal class ReadOnlyChangeTracker : ChangeTracker + { + internal override TrackedObject Track(object obj) { return null; } + internal override TrackedObject Track(object obj, bool recurse) { return null; } + internal override void FastTrack(object obj) + { + // nop + } + internal override bool IsTracked(object obj) { return false; } + internal override TrackedObject GetTrackedObject(object obj) { return null; } + internal override void StopTracking(object obj) { } + internal override void AcceptChanges() + { + // nop + } + internal override IEnumerable GetInterestingObjects() { return new TrackedObject[0]; } + } +} + diff --git a/src/ChangeManagement/StandardChangeTracker.cs b/src/ChangeManagement/StandardChangeTracker.cs new file mode 100644 index 0000000..398b8ff --- /dev/null +++ b/src/ChangeManagement/StandardChangeTracker.cs @@ -0,0 +1,991 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace System.Data.Linq +{ + using System.Data.Linq.Mapping; + using System.Data.Linq.Provider; + + internal class StandardChangeTracker : ChangeTracker + { + Dictionary items; + PropertyChangingEventHandler onPropertyChanging; + CommonDataServices services; + + internal StandardChangeTracker(CommonDataServices services) + { + this.services = services; + this.items = new Dictionary(); + this.onPropertyChanging = new PropertyChangingEventHandler(this.OnPropertyChanging); + } + + /// + /// Given a type root and a discriminator, return the type that would be instantiated. + /// + private static MetaType TypeFromDiscriminator(MetaType root, object discriminator) + { + foreach(MetaType type in root.InheritanceTypes) + { + if(IsSameDiscriminator(discriminator, type.InheritanceCode)) + return type; + } + return root.InheritanceDefault; + } + + private static bool IsSameDiscriminator(object discriminator1, object discriminator2) + { + if(discriminator1 == discriminator2) + { + return true; + } + if(discriminator1 == null || discriminator2 == null) + { + return false; + } + return discriminator1.Equals(discriminator2); + } + + internal override TrackedObject Track(object obj) + { + return Track(obj, false); + } + + internal override TrackedObject Track(object obj, bool recurse) + { + MetaType type = this.services.Model.GetMetaType(obj.GetType()); + Dictionary visited = new Dictionary(); + return Track(type, obj, visited, recurse, 1); + } + + private TrackedObject Track(MetaType mt, object obj, Dictionary visited, bool recurse, int level) + { + StandardTrackedObject tracked = (StandardTrackedObject)this.GetTrackedObject(obj); + if(tracked != null || visited.ContainsKey(obj)) + { + return tracked; + } + + // The root object tracked is tracked normally - all other objects + // in the reference graph are weakly tracked. + bool weaklyTrack = level > 1; + tracked = new StandardTrackedObject(this, mt, obj, obj, weaklyTrack); + if(tracked.HasDeferredLoaders) + { + throw Error.CannotAttachAddNonNewEntities(); + } + this.items.Add(obj, tracked); + this.Attach(obj); + visited.Add(obj, obj); + + if(recurse) + { + // track parents (objects we are dependent on) + foreach(RelatedItem parent in this.services.GetParents(mt, obj)) + { + this.Track(parent.Type, parent.Item, visited, recurse, level + 1); + } + + // track children (objects that are dependent on us) + foreach(RelatedItem child in this.services.GetChildren(mt, obj)) + { + this.Track(child.Type, child.Item, visited, recurse, level + 1); + } + } + + return tracked; + } + + internal override void FastTrack(object obj) + { + // assumes object is already in identity cache + this.Attach(obj); + } + + internal override void StopTracking(object obj) + { + this.Detach(obj); + this.items.Remove(obj); + } + + internal override bool IsTracked(object obj) + { + return this.items.ContainsKey(obj) || this.IsFastTracked(obj); + } + + private bool IsFastTracked(object obj) + { + MetaType type = this.services.Model.GetTable(obj.GetType()).RowType; + return this.services.IsCachedObject(type, obj); + } + + internal override TrackedObject GetTrackedObject(object obj) + { + StandardTrackedObject ti; + if(!this.items.TryGetValue(obj, out ti)) + { + if(this.IsFastTracked(obj)) + { + return this.PromoteFastTrackedObject(obj); + } + } + return ti; + } + + private StandardTrackedObject PromoteFastTrackedObject(object obj) + { + Type type = obj.GetType(); + MetaType metaType = this.services.Model.GetTable(type).RowType.GetInheritanceType(type); + return this.PromoteFastTrackedObject(metaType, obj); + } + + private StandardTrackedObject PromoteFastTrackedObject(MetaType type, object obj) + { + StandardTrackedObject ti = new StandardTrackedObject(this, type, obj, obj); + this.items.Add(obj, ti); + return ti; + } + + private void Attach(object obj) + { + INotifyPropertyChanging notifier = obj as INotifyPropertyChanging; + if(notifier != null) + { + notifier.PropertyChanging += this.onPropertyChanging; + } + else + { + // if has no notifier, consider it modified already + this.OnPropertyChanging(obj, null); + } + } + + private void Detach(object obj) + { + INotifyPropertyChanging notifier = obj as INotifyPropertyChanging; + if(notifier != null) + { + notifier.PropertyChanging -= this.onPropertyChanging; + } + } + + private void OnPropertyChanging(object sender, PropertyChangingEventArgs args) + { + StandardTrackedObject ti; + if(this.items.TryGetValue(sender, out ti)) + { + ti.StartTracking(); + } + else if(this.IsFastTracked(sender)) + { + ti = this.PromoteFastTrackedObject(sender); + ti.StartTracking(); + } + } + + internal override void AcceptChanges() + { + List list = new List((IEnumerable)this.items.Values); + foreach(TrackedObject item in list) + { + item.AcceptChanges(); + } + } + + internal override IEnumerable GetInterestingObjects() + { + foreach(StandardTrackedObject ti in this.items.Values) + { + if(ti.IsInteresting) + { + yield return ti; + } + } + } + + class StandardTrackedObject : TrackedObject + { + private StandardChangeTracker tracker; + private MetaType type; + private object current; + private object original; + private State state; + private BitArray dirtyMemberCache; + private bool haveInitializedDeferredLoaders; + private bool isWeaklyTracked; + + enum State + { + New, + Deleted, + PossiblyModified, + Modified, + Removed, + Dead + } + + public override string ToString() + { + return type.Name + ":" + GetState(); + } + + private string GetState() + { + switch(this.state) + { + case State.New: + case State.Deleted: + case State.Dead: + case State.Removed: + return this.state.ToString(); + default: + if(this.IsModified) + { + return "Modified"; + } + else + { + return "Unmodified"; + } + } + } + + internal StandardTrackedObject(StandardChangeTracker tracker, MetaType type, object current, object original) + { + if(current == null) + { + throw Error.ArgumentNull("current"); + } + this.tracker = tracker; + this.type = type.GetInheritanceType(current.GetType()); + this.current = current; + this.original = original; + this.state = State.PossiblyModified; + dirtyMemberCache = new BitArray(this.type.DataMembers.Count); + } + + internal StandardTrackedObject(StandardChangeTracker tracker, MetaType type, object current, object original, bool isWeaklyTracked) + : this(tracker, type, current, original) + { + this.isWeaklyTracked = isWeaklyTracked; + } + + internal override bool IsWeaklyTracked + { + get { return isWeaklyTracked; } + } + + internal override MetaType Type + { + get { return this.type; } + } + + internal override object Current + { + get { return this.current; } + } + + internal override object Original + { + get { return this.original; } + } + + internal override bool IsNew + { + get { return this.state == State.New; } + } + + internal override bool IsDeleted + { + get { return this.state == State.Deleted; } + } + + internal override bool IsRemoved + { + get { return this.state == State.Removed; } + } + + internal override bool IsDead + { + get { return this.state == State.Dead; } + } + + internal override bool IsModified + { + get { return this.state == State.Modified || (this.state == State.PossiblyModified && this.current != this.original && this.HasChangedValues()); } + } + + internal override bool IsUnmodified + { + get { return this.state == State.PossiblyModified && (this.current == this.original || !this.HasChangedValues()); } + } + + internal override bool IsPossiblyModified + { + get { return this.state == State.Modified || this.state == State.PossiblyModified; } + } + + internal override bool CanInferDelete() + { + // A delete can be inferred iff there is a non-nullable singleton association that has + // been set to null, and the association has DeleteOnNull = true. + if(this.state == State.Modified || this.state == State.PossiblyModified) + { + foreach(MetaAssociation assoc in Type.Associations) + { + if(assoc.DeleteOnNull && assoc.IsForeignKey && !assoc.IsNullable && !assoc.IsMany && + assoc.ThisMember.StorageAccessor.HasAssignedValue(Current) && + assoc.ThisMember.StorageAccessor.GetBoxedValue(Current) == null) + { + return true; + } + } + } + return false; + } + + internal override bool IsInteresting + { + get + { + return this.state == State.New || + this.state == State.Deleted || + this.state == State.Modified || + (this.state == State.PossiblyModified && this.current != this.original) || + CanInferDelete(); + } + } + + internal override void ConvertToNew() + { + // must be new or unmodified or removed to convert to new + System.Diagnostics.Debug.Assert(this.IsNew || this.IsRemoved || this.IsUnmodified); + this.original = null; + this.state = State.New; + } + + internal override void ConvertToPossiblyModified() + { + System.Diagnostics.Debug.Assert(this.IsPossiblyModified || this.IsDeleted); + this.state = State.PossiblyModified; + this.isWeaklyTracked = false; + } + + internal override void ConvertToModified() + { + System.Diagnostics.Debug.Assert(this.IsPossiblyModified); + System.Diagnostics.Debug.Assert(this.type.VersionMember != null || !this.type.HasUpdateCheck); + this.state = State.Modified; + this.isWeaklyTracked = false; + } + + internal override void ConvertToPossiblyModified(object originalState) + { + // must be modified or unmodified to convert to modified + System.Diagnostics.Debug.Assert(this.IsNew || this.IsPossiblyModified); + System.Diagnostics.Debug.Assert(originalState != null); + System.Diagnostics.Debug.Assert(originalState.GetType() == this.type.Type); + this.state = State.PossiblyModified; + this.original = this.CreateDataCopy(originalState); + this.isWeaklyTracked = false; + } + + internal override void ConvertToDeleted() + { + // must be modified or unmodified to be deleted + System.Diagnostics.Debug.Assert(this.IsDeleted || this.IsPossiblyModified); + this.state = State.Deleted; + this.isWeaklyTracked = false; + } + + internal override void ConvertToDead() + { + System.Diagnostics.Debug.Assert(this.IsDead || this.IsDeleted); + this.state = State.Dead; + this.isWeaklyTracked = false; + } + + internal override void ConvertToRemoved() + { + System.Diagnostics.Debug.Assert(this.IsRemoved || this.IsNew); + this.state = State.Removed; + this.isWeaklyTracked = false; + } + + internal override void ConvertToUnmodified() + { + System.Diagnostics.Debug.Assert(this.IsNew || this.IsPossiblyModified); + // reset to unmodified + this.state = State.PossiblyModified; + if(this.current is INotifyPropertyChanging) + { + this.original = this.current; + } + else + { + this.original = this.CreateDataCopy(this.current); + } + this.ResetDirtyMemberTracking(); + this.isWeaklyTracked = false; + } + + internal override void AcceptChanges() + { + if(IsWeaklyTracked) + { + InitializeDeferredLoaders(); + isWeaklyTracked = false; + } + if(this.IsDeleted) + { + this.ConvertToDead(); + } + else if(this.IsNew) + { + this.InitializeDeferredLoaders(); + this.ConvertToUnmodified(); + } + else if(this.IsPossiblyModified) + { + this.ConvertToUnmodified(); + } + } + + private void AssignMember(object instance, MetaDataMember mm, object value) + { + // In the unnotified case, directly use the storage accessor + // for everything because there are not events to be fired. + if(!(this.current is INotifyPropertyChanging)) + { + mm.StorageAccessor.SetBoxedValue(ref instance, value); + } + else + { + // Go through the member accessor to fire events. + mm.MemberAccessor.SetBoxedValue(ref instance, value); + } + } + + /// + /// Certain state is saved during change tracking to enable modifications + /// to be detected taking refresh operations into account. When changes + /// are reverted or accepted, this state must be reset. + /// + private void ResetDirtyMemberTracking() + { + this.dirtyMemberCache.SetAll(false); + } + + /// + /// Refresh internal tracking state using the original value and mode + /// specified. + /// + internal override void Refresh(RefreshMode mode, object freshInstance) + { + this.SynchDependentData(); + + // This must be done prior to updating original values + this.UpdateDirtyMemberCache(); + + // Apply the refresh strategy to each data member + Type instanceType = freshInstance.GetType(); + foreach(MetaDataMember mm in type.PersistentDataMembers) + { + var memberMode = mm.IsDbGenerated ? RefreshMode.OverwriteCurrentValues : mode; + if(memberMode != RefreshMode.KeepCurrentValues) + { + if(!mm.IsAssociation && (this.Type.Type == instanceType || mm.DeclaringType.Type.IsAssignableFrom(instanceType))) + { + object freshValue = mm.StorageAccessor.GetBoxedValue(freshInstance); + this.RefreshMember(mm, memberMode, freshValue); + } + } + } + + // Make the new data the current original value + this.original = this.CreateDataCopy(freshInstance); + + if(mode == RefreshMode.OverwriteCurrentValues) + { + this.ResetDirtyMemberTracking(); + } + } + + /// + /// Using the last saved comparison baseline, figure out which members have + /// changed since the last refresh, and save that information. This must be + /// done BEFORE any merge operations modify the current values. + /// + private void UpdateDirtyMemberCache() + { + // iterate over all members, and if they differ from + // last read values, mark as dirty + foreach(MetaDataMember mm in type.PersistentDataMembers) + { + if(mm.IsAssociation && mm.Association.IsMany) + { + continue; + } + if(!this.dirtyMemberCache.Get(mm.Ordinal) && this.HasChangedValue(mm)) + { + this.dirtyMemberCache.Set(mm.Ordinal, true); + } + } + } + + internal override void RefreshMember(MetaDataMember mm, RefreshMode mode, object freshValue) + { + System.Diagnostics.Debug.Assert(!mm.IsAssociation); + + if(mode == RefreshMode.KeepCurrentValues) + { + return; + } + + bool hasUserChange = this.HasChangedValue(mm); + + // we don't want to overwrite any modified values, unless + // the mode is original wins + if(hasUserChange && mode != RefreshMode.OverwriteCurrentValues) + return; + + object currentValue = mm.StorageAccessor.GetBoxedValue(this.current); + if(!object.Equals(freshValue, currentValue)) + { + mm.StorageAccessor.SetBoxedValue(ref this.current, freshValue); + + // update all singleton associations that are affected by a change to this member + foreach(MetaDataMember am in this.GetAssociationsForKey(mm)) + { + if(!am.Association.IsMany) + { + IEnumerable ds = this.tracker.services.GetDeferredSourceFactory(am).CreateDeferredSource(this.current); + if(am.StorageAccessor.HasValue(this.current)) + { + this.AssignMember(this.current, am, ds.Cast().SingleOrDefault()); + } + } + } + } + } + + private IEnumerable GetAssociationsForKey(MetaDataMember key) + { + foreach(MetaDataMember mm in this.type.PersistentDataMembers) + { + if(mm.IsAssociation && mm.Association.ThisKey.Contains(key)) + { + yield return mm; + } + } + } + + internal override object CreateDataCopy(object instance) + { + System.Diagnostics.Debug.Assert(instance != null); + Type instanceType = instance.GetType(); + System.Diagnostics.Debug.Assert(instance.GetType() == this.type.Type); + + object copy = Activator.CreateInstance(this.Type.Type); + + MetaType rootMetaType = this.tracker.services.Model.GetTable(instanceType).RowType.InheritanceRoot; + foreach(MetaDataMember mm in rootMetaType.GetInheritanceType(instanceType).PersistentDataMembers) + { + if(this.Type.Type != instanceType && !mm.DeclaringType.Type.IsAssignableFrom(instanceType)) + { + continue; + } + if(mm.IsDeferred) + { + // do not copy associations + if(!mm.IsAssociation) + { + if(mm.StorageAccessor.HasValue(instance)) + { + object value = mm.DeferredValueAccessor.GetBoxedValue(instance); + mm.DeferredValueAccessor.SetBoxedValue(ref copy, value); + } + else + { + IEnumerable ds = this.tracker.services.GetDeferredSourceFactory(mm).CreateDeferredSource(copy); + mm.DeferredSourceAccessor.SetBoxedValue(ref copy, ds); + } + } + } + else + { + // otherwise assign the value as-is to the backup instance + object value = mm.StorageAccessor.GetBoxedValue(instance); + // assumes member values are immutable or will communicate changes to entity + // note: byte[] and char[] don't do this. + mm.StorageAccessor.SetBoxedValue(ref copy, value); + } + } + return copy; + } + + internal void StartTracking() + { + if(this.original == this.current) + { + this.original = this.CreateDataCopy(this.current); + } + } + + // Return value indicates whether or not any data was actually [....]'d + internal override bool SynchDependentData() + { + bool valueWasSet = false; + + // set foreign key fields + foreach(MetaAssociation assoc in this.Type.Associations) + { + MetaDataMember mm = assoc.ThisMember; + if(assoc.IsForeignKey) + { + bool hasAssigned = mm.StorageAccessor.HasAssignedValue(this.current); + bool hasLoaded = mm.StorageAccessor.HasLoadedValue(this.current); + if(hasAssigned || hasLoaded) + { + object parent = mm.StorageAccessor.GetBoxedValue(this.current); + if(parent != null) + { + // copy parent's current primary key into this instance's foreign key fields + for(int i = 0, n = assoc.ThisKey.Count; i < n; i++) + { + MetaDataMember accThis = assoc.ThisKey[i]; + MetaDataMember accParent = assoc.OtherKey[i]; + object parentValue = accParent.StorageAccessor.GetBoxedValue(parent); + accThis.StorageAccessor.SetBoxedValue(ref this.current, parentValue); + valueWasSet = true; + } + } + else if(assoc.IsNullable) + { + if(mm.IsDeferred || (this.original != null && mm.MemberAccessor.GetBoxedValue(this.original) != null)) + { + // no known parent? set to null + for(int i = 0, n = assoc.ThisKey.Count; i < n; i++) + { + MetaDataMember accThis = assoc.ThisKey[i]; + if(accThis.CanBeNull) + { + if(this.original != null && this.HasChangedValue(accThis)) + { + if(accThis.StorageAccessor.GetBoxedValue(this.current) != null) + { + throw Error.InconsistentAssociationAndKeyChange(accThis.Member.Name, mm.Member.Name); + } + } + else + { + accThis.StorageAccessor.SetBoxedValue(ref this.current, null); + valueWasSet = true; + } + } + } + } + } + else if(!hasLoaded) + { + //Else the parent association has been set to null; but the ID is not nullable so + //the value can not be set + StringBuilder keys = new StringBuilder(); + foreach(MetaDataMember key in assoc.ThisKey) + { + if(keys.Length > 0) + { + keys.Append(", "); + } + keys.AppendFormat("{0}.{1}", this.Type.Name.ToString(), key.Name); + } + throw Error.CouldNotRemoveRelationshipBecauseOneSideCannotBeNull(assoc.OtherType.Name, this.Type.Name, keys); + } + } + } + } + + /// Explicitly set any inheritance discriminator for item. + if(this.type.HasInheritance) + { + if(this.original != null) + { + object currentDiscriminator = type.Discriminator.MemberAccessor.GetBoxedValue(this.current); + MetaType currentTypeFromDiscriminator = TypeFromDiscriminator(this.type, currentDiscriminator); + object dbDiscriminator = type.Discriminator.MemberAccessor.GetBoxedValue(this.original); + MetaType dbTypeFromDiscriminator = TypeFromDiscriminator(this.type, dbDiscriminator); + + // Would the discriminator change also change the type? If so, its not allowed. + if(currentTypeFromDiscriminator != dbTypeFromDiscriminator) + { + throw Error.CannotChangeInheritanceType(dbDiscriminator, + currentDiscriminator, original.GetType().Name, currentTypeFromDiscriminator); + } + } + else + { + // No db value means this is an 'Add'. Set the discriminator. + MetaType currentType = type.GetInheritanceType(this.current.GetType()); + if(currentType.HasInheritanceCode) + { + object code = currentType.InheritanceCode; + this.type.Discriminator.MemberAccessor.SetBoxedValue(ref current, code); + valueWasSet = true; + } + } + } + return valueWasSet; + } + + internal override bool HasChangedValue(MetaDataMember mm) + { + if(this.current == this.original) + { + return false; + } + if(mm.IsAssociation && mm.Association.IsMany) + { + return mm.StorageAccessor.HasAssignedValue(this.original); + } + if(mm.StorageAccessor.HasValue(this.current)) + { + if(this.original != null && mm.StorageAccessor.HasValue(this.original)) + { + // If the member has ever been in a modified state + // in the past, it is considered modified + if(dirtyMemberCache.Get(mm.Ordinal)) + { + return true; + } + object baseline = mm.MemberAccessor.GetBoxedValue(this.original); + object currentValue = mm.MemberAccessor.GetBoxedValue(this.current); + if(!object.Equals(currentValue, baseline)) + { + return true; + } + return false; + } + else if(mm.IsDeferred && mm.StorageAccessor.HasAssignedValue(this.current)) + { + return true; + } + } + return false; + } + + internal override bool HasChangedValues() + { + if(this.current == this.original) + { + return false; + } + if(this.IsNew) + { + return true; + } + foreach(MetaDataMember mm in this.type.PersistentDataMembers) + { + if(!mm.IsAssociation && this.HasChangedValue(mm)) + { + return true; + } + } + return false; + } + + internal override IEnumerable GetModifiedMembers() + { + foreach(MetaDataMember mm in this.type.PersistentDataMembers) + { + if(this.IsModifiedMember(mm)) + { + object currentValue = mm.MemberAccessor.GetBoxedValue(this.current); + if(this.original != null && mm.StorageAccessor.HasValue(this.original)) + { + object originalValue = mm.MemberAccessor.GetBoxedValue(this.original); + yield return new ModifiedMemberInfo(mm.Member, currentValue, originalValue); + } + else if(this.original == null || (mm.IsDeferred && !mm.StorageAccessor.HasLoadedValue(this.current))) + { + yield return new ModifiedMemberInfo(mm.Member, currentValue, null); + } + } + } + } + + private bool IsModifiedMember(MetaDataMember member) + { + return !member.IsAssociation && + !member.IsPrimaryKey && + !member.IsVersion && + !member.IsDbGenerated && + member.StorageAccessor.HasAssignedValue(this.current) && + (this.state == State.Modified || + (this.state == State.PossiblyModified && this.HasChangedValue(member))); + } + + internal override bool HasDeferredLoaders + { + get + { + foreach(MetaAssociation assoc in this.Type.Associations) + { + if(HasDeferredLoader(assoc.ThisMember)) + { + return true; + } + } + IEnumerable deferredMembers = this.Type.PersistentDataMembers.Where(p => p.IsDeferred && !p.IsAssociation); + foreach(MetaDataMember deferredMember in deferredMembers) + { + if(HasDeferredLoader(deferredMember)) + { + return true; + } + } + return false; + } + } + + private bool HasDeferredLoader(MetaDataMember deferredMember) + { + if(!deferredMember.IsDeferred) + { + return false; + } + + MetaAccessor acc = deferredMember.StorageAccessor; + if(acc.HasAssignedValue(this.current) || acc.HasLoadedValue(this.current)) + { + return false; + } + MetaAccessor dsacc = deferredMember.DeferredSourceAccessor; + IEnumerable loader = (IEnumerable)dsacc.GetBoxedValue(this.current); + + return loader != null; + } + + /// + /// Called to initialize deferred loaders for New or Attached entities. + /// + internal override void InitializeDeferredLoaders() + { + if(this.tracker.services.Context.DeferredLoadingEnabled) + { + foreach(MetaAssociation assoc in this.Type.Associations) + { + // don't set loader on association that is dependent on unrealized generated values + if(!this.IsPendingGeneration(assoc.ThisKey)) + { + InitializeDeferredLoader(assoc.ThisMember); + } + } + IEnumerable deferredMembers = this.Type.PersistentDataMembers.Where(p => p.IsDeferred && !p.IsAssociation); + foreach(MetaDataMember deferredMember in deferredMembers) + { + // don't set loader on member that is dependent on unrealized generated values + if(!this.IsPendingGeneration(Type.IdentityMembers)) + { + InitializeDeferredLoader(deferredMember); + } + } + haveInitializedDeferredLoaders = true; + } + } + + private void InitializeDeferredLoader(MetaDataMember deferredMember) + { + MetaAccessor acc = deferredMember.StorageAccessor; + if(!acc.HasAssignedValue(this.current) && !acc.HasLoadedValue(this.current)) + { + MetaAccessor dsacc = deferredMember.DeferredSourceAccessor; + IEnumerable loader = (IEnumerable)dsacc.GetBoxedValue(this.current); + // don't reset loader on any deferred member that already has one + if(loader == null) + { + IDeferredSourceFactory factory = this.tracker.services.GetDeferredSourceFactory(deferredMember); + loader = factory.CreateDeferredSource(this.current); + dsacc.SetBoxedValue(ref this.current, loader); + + } + else if(loader != null && !haveInitializedDeferredLoaders) + { + // If loader is present but wasn't generated by us, then + // an attempt to Attach or Add an entity from another context + // has been made, which is not supported. + throw Error.CannotAttachAddNonNewEntities(); + } + } + } + + internal override bool IsPendingGeneration(IEnumerable key) + { + if(this.IsNew) + { + foreach(MetaDataMember member in key) + { + if(IsMemberPendingGeneration(member)) + { + return true; + } + } + } + return false; + } + + internal override bool IsMemberPendingGeneration(MetaDataMember keyMember) + { + if(this.IsNew && keyMember.IsDbGenerated) + { + return true; + } + // look for any FK association that has this key member (should only be one) + foreach(MetaAssociation assoc in type.Associations) + { + if(assoc.IsForeignKey) + { + int index = assoc.ThisKey.IndexOf(keyMember); + if(index > -1) + { + // we must have a reference to this other object to know if its side of + // the association is generated or not + object otherItem = null; + if(assoc.ThisMember.IsDeferred) + { + otherItem = assoc.ThisMember.DeferredValueAccessor.GetBoxedValue(this.current); + } + else + { + otherItem = assoc.ThisMember.StorageAccessor.GetBoxedValue(this.current); + } + if(otherItem != null) + { + if(assoc.IsMany) + { + // Can't be pending generation for a value that would have to be the same + // across many rows. + continue; + } + else + { + StandardTrackedObject trackedOther = (StandardTrackedObject)this.tracker.GetTrackedObject(otherItem); + if(trackedOther != null) + { + MetaDataMember otherMember = assoc.OtherKey[index]; + return trackedOther.IsMemberPendingGeneration(otherMember); + } + } + } + } + } + } + return false; + } + } + } +} + diff --git a/src/SD.Tools.LinqToSQL2.csproj b/src/SD.Tools.LinqToSQL2.csproj index 5df65e3..04abd29 100644 --- a/src/SD.Tools.LinqToSQL2.csproj +++ b/src/SD.Tools.LinqToSQL2.csproj @@ -51,6 +51,8 @@ + +