From 8beefab7a73445cca97ce825c56e228fe69e9bbb Mon Sep 17 00:00:00 2001 From: Frans Bouma Date: Mon, 29 Dec 2014 12:30:51 +0100 Subject: [PATCH] Moved the change tracker implementations to their own file They were private classes but their implementation is significant. They're now internal instead of private so the parent class isn't used as a private namespace. --- src/ChangeManagement/ChangeTracker.cs | 994 ------------------ src/ChangeManagement/ReadOnlyChangeTracker.cs | 35 + src/ChangeManagement/StandardChangeTracker.cs | 991 +++++++++++++++++ src/SD.Tools.LinqToSQL2.csproj | 2 + 4 files changed, 1028 insertions(+), 994 deletions(-) create mode 100644 src/ChangeManagement/ReadOnlyChangeTracker.cs create mode 100644 src/ChangeManagement/StandardChangeTracker.cs 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 @@ + +