File: ChangeTracker.cs
Project: ndp\fx\src\DLinq\Dlinq\System.Data.Linq.csproj (System.Data.Linq)
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 abstract class ChangeTracker {
        /// <summary>
        /// Starts tracking an object as 'unchanged'
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        internal abstract TrackedObject Track(object obj);
        /// <summary>
        /// Starts tracking an object as 'unchanged', and optionally
        /// 'weakly' tracks all other referenced objects recursively.
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="recurse">True if all untracked objects in the graph
        /// should be tracked recursively.</param>
        /// <returns></returns>
        internal abstract TrackedObject Track(object obj, bool recurse);
        /// <summary>
        /// Fast-tracks an object that is already in identity cache
        /// </summary>
        /// <param name="obj"></param>
        internal abstract void FastTrack(object obj);
        internal abstract bool IsTracked(object obj);
        internal abstract TrackedObject GetTrackedObject(object obj);
        internal abstract void StopTracking(object obj);
        internal abstract void AcceptChanges();
        internal abstract IEnumerable<TrackedObject> GetInterestingObjects();
 
        internal static ChangeTracker CreateChangeTracker(CommonDataServices dataServices, bool asReadOnly) {
            if (asReadOnly) {
                return new ReadOnlyChangeTracker();
            }
            else {
                return new StandardChangeTracker(dataServices);
            }
        }
 
        class StandardChangeTracker : ChangeTracker {
            Dictionary<object, StandardTrackedObject> items;
            PropertyChangingEventHandler onPropertyChanging;
            CommonDataServices services;
 
            internal StandardChangeTracker(CommonDataServices services) {
                this.services = services;
                this.items = new Dictionary<object, StandardTrackedObject>();
                this.onPropertyChanging = new PropertyChangingEventHandler(this.OnPropertyChanging);
            }
 
            /// <summary>
            /// Given a type root and a discriminator, return the type that would be instantiated.
            /// </summary>
            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<object, object> visited = new Dictionary<object, object>();
                return Track(type, obj, visited, recurse, 1);
            }
 
            private TrackedObject Track(MetaType mt, object obj, Dictionary<object, object> 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<StandardTrackedObject> list = new List<StandardTrackedObject>((IEnumerable<StandardTrackedObject>)this.items.Values);
                foreach (TrackedObject item in list) {
                    item.AcceptChanges();
                }
            }
 
            internal override IEnumerable<TrackedObject> 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);
                    }
                }
 
                /// <summary>
                /// 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.
                /// </summary>
                private void ResetDirtyMemberTracking() {
                    this.dirtyMemberCache.SetAll(false);
                }
 
                /// <summary>
                /// Refresh internal tracking state using the original value and mode
                /// specified.
                /// </summary>        
                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();
                    }
                }
 
                /// <summary>
                /// 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.
                /// </summary>
                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<Object>().SingleOrDefault());
                                }
                            }
                        }
                    }
                }
 
                private IEnumerable<MetaDataMember> 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 sync'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<ModifiedMemberInfo> 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<MetaDataMember> 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;
                }
 
                /// <summary>
                /// Called to initialize deferred loaders for New or Attached entities.
                /// </summary>
                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<MetaDataMember> 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<MetaDataMember> 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;
                }
            }
        }
 
        /// <summary>
        /// This is the implementation used when change tracking is disabled.
        /// </summary>
        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<TrackedObject> GetInterestingObjects() { return new TrackedObject[0]; }
        }
    }
 
    internal abstract class TrackedObject {
        internal abstract MetaType Type { get; }
        /// <summary>
        /// The current client value.
        /// </summary>
        internal abstract object Current { get; }
        /// <summary>
        /// The last read database value.  This is updated whenever the
        /// item is refreshed.
        /// </summary>
        internal abstract object Original { get; }
        internal abstract bool IsInteresting { get; } // new, deleted or possibly changed
        internal abstract bool IsNew { get; }
        internal abstract bool IsDeleted { get; }
        internal abstract bool IsModified { get; }
        internal abstract bool IsUnmodified { get; }
        internal abstract bool IsPossiblyModified { get; }
        internal abstract bool IsRemoved { get; }
        internal abstract bool IsDead { get; }
        /// <summary>
        /// True if the object is being tracked (perhaps during a recursive
        /// attach operation) but can be transitioned to other states.
        /// </summary>
        internal abstract bool IsWeaklyTracked { get; }
        internal abstract bool HasDeferredLoaders { get; }        
        internal abstract bool HasChangedValues();
        internal abstract IEnumerable<ModifiedMemberInfo> GetModifiedMembers();
        internal abstract bool HasChangedValue(MetaDataMember mm);
        internal abstract bool CanInferDelete();
        internal abstract void AcceptChanges();
        internal abstract void ConvertToNew();
        internal abstract void ConvertToPossiblyModified();
        internal abstract void ConvertToPossiblyModified(object original);
        internal abstract void ConvertToUnmodified();
        internal abstract void ConvertToModified();
        internal abstract void ConvertToDeleted();
        internal abstract void ConvertToRemoved();
        internal abstract void ConvertToDead();
        /// <summary>
        /// Refresh the item by making the value passed in the current 
        /// Database value, and refreshing the current values using the
        /// mode specified.
        /// </summary>       
        internal abstract void Refresh(RefreshMode mode, object freshInstance);
        /// <summary>
        /// Does the refresh operation for a single member.  This method does not 
        /// update the baseline 'original' value.  You must call 
        /// Refresh(RefreshMode.KeepCurrentValues, freshInstance) to finish the refresh 
        /// after refreshing individual members.
        /// </summary>
        /// <param name="member"></param>
        /// <param name="mode"></param>
        /// <param name="freshValue"></param>
        internal abstract void RefreshMember(MetaDataMember member, RefreshMode mode, object freshValue);
        /// <summary>
        /// Create a data-member only copy of the instance (no associations)
        /// </summary>
        /// <returns></returns>
        internal abstract object CreateDataCopy(object instance);
 
        internal abstract bool SynchDependentData();
 
        internal abstract bool IsPendingGeneration(IEnumerable<MetaDataMember> keyMembers);
        internal abstract bool IsMemberPendingGeneration(MetaDataMember keyMember);
 
        internal abstract void InitializeDeferredLoaders();
    }
}