using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Diagnostics;
namespace System.Data.Linq {
using System.Data.Linq.Mapping;
using System.Data.Linq.Provider;
///
/// Describes the type of change the entity will undergo when submitted to the database.
///
public enum ChangeAction {
///
/// The entity will not be submitted.
///
None = 0,
///
/// The entity will be deleted.
///
Delete,
///
/// The entity will be inserted.
///
Insert,
///
/// The entity will be updated.
///
Update
}
internal class ChangeProcessor {
CommonDataServices services;
DataContext context;
ChangeTracker tracker;
ChangeDirector changeDirector;
EdgeMap currentParentEdges;
EdgeMap originalChildEdges;
ReferenceMap originalChildReferences;
internal ChangeProcessor(CommonDataServices services, DataContext context) {
this.services = services;
this.context = context;
this.tracker = services.ChangeTracker;
this.changeDirector = services.ChangeDirector;
this.currentParentEdges = new EdgeMap();
this.originalChildEdges = new EdgeMap();
this.originalChildReferences = new ReferenceMap();
}
internal void SubmitChanges(ConflictMode failureMode) {
this.TrackUntrackedObjects();
// Must apply inferred deletions only after any untracked objects
// are tracked
this.ApplyInferredDeletions();
this.BuildEdgeMaps();
var list = this.GetOrderedList();
ValidateAll(list);
int numUpdatesAttempted = 0;
ChangeConflictSession conflictSession = new ChangeConflictSession(this.context);
List conflicts = new List();
List deletedItems = new List();
List insertedItems = new List();
List syncDependentItems = new List();
foreach (TrackedObject item in list) {
try {
if (item.IsNew) {
if (item.SynchDependentData()) {
syncDependentItems.Add(item);
}
changeDirector.Insert(item);
// store all inserted items for post processing
insertedItems.Add(item);
}
else if (item.IsDeleted) {
// Delete returns 1 if the delete was successfull, 0 if the row exists
// but wasn't deleted due to an OC conflict, or -1 if the row was
// deleted by another context (no OC conflict in this case)
numUpdatesAttempted++;
int ret = changeDirector.Delete(item);
if (ret == 0) {
conflicts.Add(new ObjectChangeConflict(conflictSession, item, false));
}
else {
// store all deleted items for post processing
deletedItems.Add(item);
}
}
else if (item.IsPossiblyModified) {
if (item.SynchDependentData()) {
syncDependentItems.Add(item);
}
if (item.IsModified) {
CheckForInvalidChanges(item);
numUpdatesAttempted++;
if (changeDirector.Update(item) <= 0) {
conflicts.Add(new ObjectChangeConflict(conflictSession, item));
}
}
}
}
catch (ChangeConflictException) {
conflicts.Add(new ObjectChangeConflict(conflictSession, item));
}
if (conflicts.Count > 0 && failureMode == ConflictMode.FailOnFirstConflict) {
break;
}
}
// if we have accumulated any failed updates, throw the exception now
if (conflicts.Count > 0) {
// First we need to rollback any value that have already been auto-sync'd, since the values are no longer valid on the server
changeDirector.RollbackAutoSync();
// Also rollback any dependent items that were sync'd, since their parent values may have been rolled back
foreach (TrackedObject syncDependentItem in syncDependentItems) {
Debug.Assert(syncDependentItem.IsNew || syncDependentItem.IsPossiblyModified, "SynchDependent data should only be rolled back for new and modified objects.");
syncDependentItem.SynchDependentData();
}
this.context.ChangeConflicts.Fill(conflicts);
throw CreateChangeConflictException(numUpdatesAttempted, conflicts.Count);
}
else {
// No conflicts occurred, so we don't need to save the rollback values anymore
changeDirector.ClearAutoSyncRollback();
}
// Only after all updates have been sucessfully processed do we want to make
// post processing modifications to the objects and/or cache state.
PostProcessUpdates(insertedItems, deletedItems);
}
private void PostProcessUpdates(List insertedItems, List deletedItems) {
// perform post delete processing
foreach (TrackedObject deletedItem in deletedItems) {
// remove deleted item from identity cache
this.services.RemoveCachedObjectLike(deletedItem.Type, deletedItem.Original);
ClearForeignKeyReferences(deletedItem);
}
// perform post insert processing
foreach (TrackedObject insertedItem in insertedItems) {
object lookup = this.services.InsertLookupCachedObject(insertedItem.Type, insertedItem.Current);
if (lookup != insertedItem.Current) {
throw new DuplicateKeyException(insertedItem.Current, Strings.DatabaseGeneratedAlreadyExistingKey);
}
insertedItem.InitializeDeferredLoaders();
}
}
///
/// Clears out the foreign key values and parent object references for deleted objects on the child side of a relationship.
/// For bi-directional relationships, also performs the following fixup:
/// - for 1:N we remove the deleted entity from the opposite EntitySet or collection
/// - for 1:1 we null out the back reference
///
private void ClearForeignKeyReferences(TrackedObject to) {
Debug.Assert(to.IsDeleted, "Foreign key reference cleanup should only happen on Deleted objects.");
foreach (MetaAssociation assoc in to.Type.Associations) {
if (assoc.IsForeignKey) {
// If there is a member on the other side referring back to us (i.e. this is a bi-directional relationship),
// we want to do a cache lookup to find the other side, then will remove ourselves from that collection.
// This cache lookup is only possible if the other key is the primary key, since that is the only way items can be found in the cache.
if (assoc.OtherMember != null && assoc.OtherKeyIsPrimaryKey) {
Debug.Assert(assoc.OtherMember.IsAssociation, "OtherMember of the association is expected to also be an association.");
// Search the cache for the target of the association, since
// it might not be loaded on the object being deleted, and we
// don't want to force a load.
object[] keyValues = CommonDataServices.GetForeignKeyValues(assoc, to.Current);
object cached = this.services.IdentityManager.Find(assoc.OtherType, keyValues);
if (cached != null) {
if (assoc.OtherMember.Association.IsMany) {
// Note that going through the IList interface handles
// EntitySet as well as POCO collections that implement IList
// and are not FixedSize.
System.Collections.IList collection = assoc.OtherMember.MemberAccessor.GetBoxedValue(cached) as System.Collections.IList;
if (collection != null && !collection.IsFixedSize) {
collection.Remove(to.Current);
// Explicitly clear the foreign key values and parent object reference
ClearForeignKeysHelper(assoc, to.Current);
}
}
else {
// Null out the other association. Since this is a 1:1 association,
// we're not concerned here with causing a deferred load, since the
// target is already cached (since we're deleting it).
assoc.OtherMember.MemberAccessor.SetBoxedValue(ref cached, null);
// Explicitly clear the foreign key values and parent object reference
ClearForeignKeysHelper(assoc, to.Current);
}
}
// else the item was not found in the cache, so there is no fixup that has to be done
// We are explicitly not calling ClearForeignKeysHelper because it breaks existing shipped behavior and we want to maintain backward compatibility
}
else {
// This is a unidirectional relationship or we have no way to look up the other side in the cache, so just clear our own side
ClearForeignKeysHelper(assoc, to.Current);
}
}
// else this is not the 1-side (foreign key) of the relationship, so there is nothing for us to do
}
}
// Ensure the the member and foreign keys are nulled so that after trackedInstance is deleted,
// the object does not appear to be associated with the other side anymore. This prevents the deleted object
// from referencing objects still in the cache, but also will prevent the related object from being implicitly loaded
private static void ClearForeignKeysHelper(MetaAssociation assoc, object trackedInstance) {
Debug.Assert(assoc.IsForeignKey, "Foreign key clearing should only happen on foreign key side of the association.");
Debug.Assert(assoc.ThisMember.IsAssociation, "Expected ThisMember of an association to always be an association.");
// If this member is one of our deferred loaders, and it does not already have a value, explicitly set the deferred source to
// null so that when we set the association member itself to null later, it doesn't trigger an implicit load.
// This is only necessary if the value has not already been assigned or set, because otherwise we won't implicitly load anyway when the member is accessed.
MetaDataMember thisMember = assoc.ThisMember;
if (thisMember.IsDeferred &&
!(thisMember.StorageAccessor.HasAssignedValue(trackedInstance) || thisMember.StorageAccessor.HasLoadedValue(trackedInstance)))
{
// If this is a deferred member, set the value directly in the deferred accessor instead of going
// through the normal member accessor, so that we don't trigger an implicit load.
thisMember.DeferredSourceAccessor.SetBoxedValue(ref trackedInstance, null);
}
// Notify the object that the relationship should be considered deleted.
// This allows the object to do its own fixup even when we can't do it automatically.
thisMember.MemberAccessor.SetBoxedValue(ref trackedInstance, null);
// Also set the foreign key values to null if possible
for (int i = 0, n = assoc.ThisKey.Count; i < n; i++) {
MetaDataMember thisKey = assoc.ThisKey[i];
if (thisKey.CanBeNull) {
thisKey.StorageAccessor.SetBoxedValue(ref trackedInstance, null);
}
}
}
private static void ValidateAll(IEnumerable list) {
foreach (var item in list) {
if (item.IsNew) {
item.SynchDependentData();
if (item.Type.HasAnyValidateMethod) {
SendOnValidate(item.Type, item, ChangeAction.Insert);
}
} else if (item.IsDeleted) {
if (item.Type.HasAnyValidateMethod) {
SendOnValidate(item.Type, item, ChangeAction.Delete);
}
} else if (item.IsPossiblyModified) {
item.SynchDependentData();
if (item.IsModified && item.Type.HasAnyValidateMethod) {
SendOnValidate(item.Type, item, ChangeAction.Update);
}
}
}
}
private static void SendOnValidate(MetaType type, TrackedObject item, ChangeAction changeAction) {
if (type != null) {
SendOnValidate(type.InheritanceBase, item, changeAction);
if (type.OnValidateMethod != null) {
try {
type.OnValidateMethod.Invoke(item.Current, new object[] { changeAction });
} catch (TargetInvocationException tie) {
if (tie.InnerException != null) {
throw tie.InnerException;
}
throw;
}
}
}
}
internal string GetChangeText() {
this.ObserveUntrackedObjects();
// Must apply inferred deletions only after any untracked objects
// are tracked
this.ApplyInferredDeletions();
this.BuildEdgeMaps();
// append change text only
StringBuilder changeText = new StringBuilder();
foreach (TrackedObject item in this.GetOrderedList()) {
if (item.IsNew) {
item.SynchDependentData();
changeDirector.AppendInsertText(item, changeText);
}
else if (item.IsDeleted) {
changeDirector.AppendDeleteText(item, changeText);
}
else if (item.IsPossiblyModified) {
item.SynchDependentData();
if (item.IsModified) {
changeDirector.AppendUpdateText(item, changeText);
}
}
}
return changeText.ToString();
}
internal ChangeSet GetChangeSet() {
List