//----------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//
// @owner [....]
// @backupOwner [....]
//---------------------------------------------------------------------
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Data.Common;
using System.Data.Common.CommandTrees;
using System.Data.Common.Utils;
using System.Data.Metadata.Edm;
using System.Data.Objects.Internal;
using System.Diagnostics;
using System.Text;
using System.Runtime.Serialization;
namespace System.Data.Objects.DataClasses
{
///
/// Base class for EntityCollection and EntityReference
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
[DataContract]
[Serializable]
public abstract class RelatedEnd : IRelatedEnd
{
//-----------------
// Internal Constructors
//-----------------
///
/// The default constructor is required for some serialization scenarios with EntityReference.
///
internal RelatedEnd()
{
}
internal RelatedEnd(IEntityWithRelationships owner, RelationshipNavigation navigation, IRelationshipFixer relationshipFixer)
{
EntityUtil.CheckArgumentNull(owner, "owner");
EntityUtil.CheckArgumentNull(navigation, "navigation");
EntityUtil.CheckArgumentNull(relationshipFixer, "fixer");
InitializeRelatedEnd(owner, navigation, relationshipFixer);
}
// ------
// Fields
// ------
private const string _entityKeyParamName = "EntityKeyValue";
// The following fields are serialized. Adding or removing a serialized field is considered
// a breaking change. This includes changing the field type or field name of existing
// serialized fields. If you need to make this kind of change, it may be possible, but it
// will require some custom serialization/deserialization code.
// These fields should not be changed once they have been initialized with non-null values, but they can't be read-only because there
// are serialization scenarios where they have to be set after construction
private IEntityWithRelationships _owner;
private RelationshipNavigation _navigation;
private IRelationshipFixer _relationshipFixer;
internal bool _isLoaded;
// The fields in this group are set only when attached to a context, so we don't need to serialize.
[NonSerialized]
private RelationshipSet _relationshipSet;
[NonSerialized]
private ObjectContext _context;
[NonSerialized]
private bool _usingNoTracking;
[NonSerialized]
private RelationshipType _relationMetadata;
[NonSerialized]
private RelationshipEndMember _fromEndProperty; //owner end property
[NonSerialized]
private RelationshipEndMember _toEndProperty;
[NonSerialized]
private string _sourceQuery;
[NonSerialized]
internal bool _suppressEvents;
[NonSerialized]
internal CollectionChangeEventHandler _onAssociationChanged;
// ------
// Events
// ------
///
/// Event to notify changes in the Associations.
///
public event CollectionChangeEventHandler AssociationChanged
{
add
{
CheckOwnerNull();
_onAssociationChanged += value;
}
remove
{
CheckOwnerNull();
_onAssociationChanged -= value;
}
}
/// internal event to notify change in collection
internal virtual event CollectionChangeEventHandler AssociationChangedForObjectView
{
// we fire this event only from EntityCollection, definetely not from EntityReference
add { Debug.Assert(false, "should never happen"); }
remove { Debug.Assert(false, "should never happen"); }
}
// ----------
// Properties
// ----------
///
/// This class describes a relationship navigation from the
/// navigation property on one entity to another entity.
/// RelationshipNavigation uniquely identify a relationship type.
/// The RelationshipNavigation class is internal only, so this property is also internal.
/// See RelationshipName, SourceRoleName, and TargetRoleName for the public exposure
/// of the information contained in this RelationshipNavigation.
///
internal RelationshipNavigation RelationshipNavigation
{
get
{
return _navigation;
}
}
///
/// Name of the relationship in which this RelatedEnd is participating
///
[System.Xml.Serialization.SoapIgnore]
[System.Xml.Serialization.XmlIgnore]
public string RelationshipName
{
get
{
CheckOwnerNull();
return _navigation.RelationshipName;
}
}
///
/// Name of the relationship source role used to generate this RelatedEnd
///
[System.Xml.Serialization.SoapIgnore]
[System.Xml.Serialization.XmlIgnore]
public string SourceRoleName
{
get
{
CheckOwnerNull();
return _navigation.From;
}
}
///
/// Name of the relationship target role used to generate this RelatedEnd
///
[System.Xml.Serialization.SoapIgnore]
[System.Xml.Serialization.XmlIgnore]
public string TargetRoleName
{
get
{
CheckOwnerNull();
return _navigation.To;
}
}
IEnumerable IRelatedEnd.CreateSourceQuery()
{
CheckOwnerNull();
return this.CreateSourceQueryInternal();
}
internal IEntityWithRelationships Owner
{
get
{
return this._owner;
}
}
internal ObjectContext ObjectContext
{
get
{
return this._context;
}
}
internal virtual void BulkDeleteAll(System.Collections.Generic.List list)
{
throw EntityUtil.NotSupported();
}
///
/// Returns the relationship metadata associated with this RelatedEnd.
/// This value is available once the RelatedEnd is attached to an ObjectContext
/// or is retrieved with MergeOption.NoTracking
///
[System.Xml.Serialization.SoapIgnore]
[System.Xml.Serialization.XmlIgnore]
public RelationshipSet RelationshipSet
{
get
{
CheckOwnerNull();
return this._relationshipSet;
}
}
internal RelationshipType RelationMetadata
{
get
{
return this._relationMetadata;
}
}
internal RelationshipEndMember ToEndMember
{
get
{
return this._toEndProperty;
}
}
internal bool UsingNoTracking
{
get
{
return this._usingNoTracking;
}
}
internal MergeOption DefaultMergeOption
{
get
{
return UsingNoTracking ? MergeOption.NoTracking : MergeOption.AppendOnly;
}
}
internal RelationshipEndMember FromEndProperty
{
get
{
return this._fromEndProperty;
}
}
///
/// IsLoaded returns true if and only if Load was called.
///
[System.Xml.Serialization.SoapIgnore]
[System.Xml.Serialization.XmlIgnore]
public bool IsLoaded
{
get
{
CheckOwnerNull();
return this._isLoaded;
}
}
internal void SetIsLoaded(bool value)
{
this._isLoaded = value;
}
///
/// This is the query which represents the source of the
/// related end. It is constructed on demand using the
/// _connection and _cache fields and a query string based on
/// the type of related end and the metadata passed into its
/// constructor indicating the particular EDM construct the
/// related end models. This method is called by both subclasses of this type
/// and those subclasses pass in their generic type parameter in order
/// to produce an ObjectQuery of the right type. This allows this common
/// functionality to be implemented here in the base class while still
/// allowing the base class to be non-generic.
///
/// MergeOption to use when creating the query
internal ObjectQuery CreateSourceQuery(MergeOption mergeOption)
{
// must have a context
if (_context == null)
{
return null;
}
ObjectStateEntry stateEntry = _context.ObjectStateManager.FindObjectStateEntry(_owner);
EntityState entityState;
if (stateEntry == null)
{
if (UsingNoTracking)
{
entityState = EntityState.Detached;
}
else
{
throw EntityUtil.InvalidEntityStateSource();
}
}
else
{
Debug.Assert(stateEntry != null, "Entity should exist in the current context");
entityState = stateEntry.State;
}
//Throw incase entity is in added state
if (entityState == EntityState.Added)
{
throw EntityUtil.InvalidEntityStateSource();
}
Debug.Assert(!(entityState != EntityState.Detached && UsingNoTracking), "Entity with NoTracking option cannot exist in the ObjectStateManager");
// the CreateSourceQuery method can only return non-NULL when we're
// either detached & mergeOption is NoTracking or
// Modified/Unchanged/Deleted and mergeOption is NOT NoTracking
// (if entity is attached to the context, mergeOption should never be NoTracking)
if (!((entityState == EntityState.Detached && UsingNoTracking) ||
entityState == EntityState.Modified ||
entityState == EntityState.Unchanged ||
entityState == EntityState.Deleted))
{
return null;
}
// Construct a new source query and return it.
Debug.Assert(_relationshipSet != null, "If we are attached to a context, we should have a relationship set.");
// Retrieve the entity key of the owner.
EntityKey key = _context.ObjectStateManager.GetEntityKey(_owner);
// If the source query text has not be initialized, then do so now.
if (null == _sourceQuery)
{
// Translate to:
// SELECT VALUE [TargetEntity]
// FROM
// (SELECT VALUE x FROM ##RelationshipSet AS x
// WHERE Key(x.[##SourceRoleName]) = ROW(@key1 AS key1[..., @keyN AS keyN])
// ) AS [AssociationEntry]
// INNER JOIN
// OfType(##TargetEntityset, ##TargetRole.EntityType) AS [TargetEntity]
// ON
// Key([AssociationEntry].##TargetRoleName) = Key(Ref([TargetEntity]))
//
// Note that the OfType operator can be omitted if the element type of ##TargetEntitySet
// is equal to the Entity type produced by the target end of the relationship.
Debug.Assert(_relationshipSet.BuiltInTypeKind == BuiltInTypeKind.AssociationSet, "Non-AssociationSet Relationship Set?");
EntitySet targetEntitySet = ((AssociationSet)_relationshipSet).AssociationSetEnds[_toEndProperty.Name].EntitySet;
StringBuilder sourceBuilder = new StringBuilder("SELECT VALUE [TargetEntity] FROM (SELECT VALUE x FROM ");
sourceBuilder.Append("[");
sourceBuilder.Append(_relationshipSet.EntityContainer.Name);
sourceBuilder.Append("].[");
sourceBuilder.Append(_relationshipSet.Name);
sourceBuilder.Append("] AS x WHERE Key(x.[");
sourceBuilder.Append(_fromEndProperty.Name);
sourceBuilder.Append("]) = ROW(");
AliasGenerator keyParamNameGen = new AliasGenerator(_entityKeyParamName); // Aliases are cached in AliasGenerator
int keyMemberCount = key.GetEntitySet(ObjectContext.MetadataWorkspace).ElementType.KeyMembers.Count;
for(int idx = 0; idx < keyMemberCount; idx++)
{
string keyParamName = keyParamNameGen.Next();
sourceBuilder.Append("@");
sourceBuilder.Append(keyParamName);
sourceBuilder.Append(" AS ");
sourceBuilder.Append(keyParamName);
if(idx < keyMemberCount - 1)
{
sourceBuilder.Append(",");
}
}
sourceBuilder.Append(")) AS [AssociationEntry] INNER JOIN ");
EntityType targetEntityType = MetadataHelper.GetEntityTypeForEnd((AssociationEndMember)_toEndProperty);
bool ofTypeRequired = false;
if(!targetEntitySet.ElementType.EdmEquals(targetEntityType) &&
!TypeSemantics.IsSubTypeOf(targetEntitySet.ElementType, targetEntityType))
{
// If the type contained in the target entity set is not equal to
// or a subtype of the referenced type, then an OfType must be
// applied to the target entityset to yield only those elements that
// are of the referenced type or a subtype of the referenced type.
ofTypeRequired = true;
// The type name used in the OfType clause must be the name of the
// corresponding O-Space Entity type, since the source query will be
// parsed using the CLR perspective (by ObjectQuery).
TypeUsage targetOSpaceTypeUsage = ObjectContext.MetadataWorkspace.GetOSpaceTypeUsage(TypeUsage.Create(targetEntityType));
targetEntityType = (EntityType)targetOSpaceTypeUsage.EdmType;
}
if(ofTypeRequired)
{
sourceBuilder.Append("OfType(");
}
sourceBuilder.Append("[");
sourceBuilder.Append(targetEntitySet.EntityContainer.Name);
sourceBuilder.Append("].[");
sourceBuilder.Append(targetEntitySet.Name);
sourceBuilder.Append("]");
if(ofTypeRequired)
{
sourceBuilder.Append(", [");
if (targetEntityType.NamespaceName != string.Empty)
{
sourceBuilder.Append(targetEntityType.NamespaceName);
sourceBuilder.Append("].[");
}
sourceBuilder.Append(targetEntityType.Name);
sourceBuilder.Append("])");
}
sourceBuilder.Append(" AS [TargetEntity] ON Key([AssociationEntry].[");
sourceBuilder.Append(_toEndProperty.Name);
sourceBuilder.Append("]) = Key(Ref([TargetEntity]))");
_sourceQuery = sourceBuilder.ToString();
}
// Create a new ObjectQuery based on the source query text, the object context, and the specfied merge option.
ObjectQuery query = new ObjectQuery(_sourceQuery, _context, mergeOption);
// Add a parameter for each entity key value found on the key.
AliasGenerator paramNameGen = new AliasGenerator(_entityKeyParamName); // Aliases are cached in AliasGenerator
ReadOnlyMetadataCollection.Enumerator keyMembers = key.GetEntitySet(ObjectContext.MetadataWorkspace).ElementType.KeyMembers.GetEnumerator();
foreach (EntityKeyMember keyValue in key.EntityKeyValues)
{
// Move on to the key member that corresponds to the current key value.
keyMembers.MoveNext();
// Create a new ObjectParameter with the next parameter name and the next key value.
ObjectParameter queryParam = new ObjectParameter(paramNameGen.Next(), keyValue.Value);
// Map the type of the key member to S-Space and explicitly specify this mapped type
// as the effective type of the new ObjectParameter - this is required so that the
// type of the key value parameter matches the declared type of the key member when
// the query text is parsed.
queryParam.TypeUsage = Helper.GetModelTypeUsage(keyMembers.Current);
// Add the new parameter to the Parameters collection of the query.
query.Parameters.Add(queryParam);
}
// It should not be possible to add or remove parameters from the new query, since the query text
// is fixed. Adding or removing parameters will likely make the query fail to execute.
query.Parameters.SetReadOnly(true);
// Return the new ObjectQuery. Note that this is intentionally a tear-off so that any changes made
// to its Parameters collection (or the ObjectParameters themselves) have no effect on anyone else
// that may retrieve this query - each access will always return a new ObjectQuery instance.
return query;
}
///
/// Validates that a call to Load has the correct conditions
/// This helps to reduce the complexity of the Load call (SQLBU 524128)
///
/// See RelatedEnd.CreateSourceQuery method. This is returned here so we can create it and validate the state before returning it to the caller
protected ObjectQuery ValidateLoad(MergeOption mergeOption, string relatedEndName)
{
ObjectQuery sourceQuery = CreateSourceQuery(mergeOption);
if (null == sourceQuery)
{
throw EntityUtil.RelatedEndNotAttachedToContext(relatedEndName);
}
ObjectStateEntry entry = ObjectContext.ObjectStateManager.FindObjectStateEntry(this.Owner);
//Throw in case entity is in deleted state
if (entry != null && entry.State == EntityState.Deleted)
{
throw EntityUtil.InvalidEntityStateLoad(relatedEndName);
}
// MergeOption for Load must be NoTracking if and only if the source entity was NoTracking. If the source entity was
// retrieved with any other MergeOption, the Load MergeOption can be anything but NoTracking. I.e. The entity could
// have been loaded with OverwriteChanges and the Load option can be AppendOnly.
if (UsingNoTracking != (mergeOption == MergeOption.NoTracking))
{
throw EntityUtil.MismatchedMergeOptionOnLoad(mergeOption);
}
if (UsingNoTracking)
{
if (this.IsLoaded)
{
throw EntityUtil.LoadCalledOnAlreadyLoadedNoTrackedRelatedEnd();
}
if (!IsEmpty())
{
throw EntityUtil.LoadCalledOnNonEmptyNoTrackedRelatedEnd();
}
}
return sourceQuery;
}
// -------
// Methods
// -------
///
/// Loads the related entity or entities into the local related end using the default merge option.
///
public void Load()
{
CheckOwnerNull();
Load(DefaultMergeOption);
}
///
/// Loads the related entity or entities into the local related end using the supplied MergeOption.
///
public abstract void Load(MergeOption mergeOption);
///
/// Takes a list of related entities and merges them into the current collection.
///
/// Entities to relate to the owner of this EntityCollection
/// MergeOption to use when updating existing relationships
/// Indicates whether IsLoaded should be set to true after the Load is complete.
/// Should be false in cases where we cannot guarantee that the set of entities is complete
/// and matches the server, such as Attach.
protected void Merge(IEnumerable collection, MergeOption mergeOption, bool setIsLoaded)
{
//Dev note: do not add event firing in Merge API, if it need to be added, add it to the caller
List refreshedCollection = collection as List;
if (null == refreshedCollection)
{
refreshedCollection = new List(collection);
}
EntityKey sourceKey = ObjectStateManager.FindKeyOnEntityWithRelationships(Owner);
EntityUtil.CheckEntityKeyNull(sourceKey);
ObjectStateManager.UpdateRelationships(this.ObjectContext, mergeOption, (AssociationSet)RelationshipSet, (AssociationEndMember)FromEndProperty, sourceKey, this.Owner, (AssociationEndMember)ToEndMember, refreshedCollection, setIsLoaded);
if (setIsLoaded)
{
// If the input collection contains all related entities, mark the collection as "loaded"
_isLoaded = true;
}
}
///
/// Attaches an entity to the related end. If the related end is already filled
/// or partially filled, this merges the existing entities with the given entity. The given
/// entity is not assumed to be the complete set of related entities.
///
/// Owner and all entities passed in must be in Unchanged or Modified state.
/// Deleted elements are allowed only when the state manager is already tracking the relationship
/// instance.
///
/// The entity to attach to the related end
/// Thrown when is null.
/// Thrown when the entity cannot be related via the current relationship end.
void IRelatedEnd.Attach(IEntityWithRelationships entity)
{
CheckOwnerNull();
EntityUtil.CheckArgumentNull(entity, "entity");
Attach(new IEntityWithRelationships[] { entity }, false);
}
internal protected void Attach(IEnumerable entities, bool allowCollection)
{
ValidateOwnerForAttach();
// validate children and collect them in the "refreshedCollection" for this instance
int index = 0;
List collection = new List();
foreach (TEntity entity in entities)
{
ValidateEntityForAttach(entity, index++, allowCollection);
collection.Add(entity);
}
_suppressEvents = true;
try
{
// After Attach, the two entities should be related in the Unchanged state, so use OverwriteChanges
// Since no query is done in this case, the MergeOption only controls the relationships
Merge(collection, MergeOption.OverwriteChanges, false /*setIsLoaded*/);
}
finally
{
_suppressEvents = false;
}
OnAssociationChanged(CollectionChangeAction.Refresh, null);
}
// verifies requirements for Owner in Attach()
internal protected void ValidateOwnerForAttach()
{
if (null == this.ObjectContext || UsingNoTracking)
{
throw EntityUtil.InvalidOwnerStateForAttach();
}
// find state entry
ObjectStateEntry stateEntry = this.ObjectContext.ObjectStateManager.GetObjectStateEntry(this.Owner);
if (stateEntry.State != EntityState.Modified &&
stateEntry.State != EntityState.Unchanged)
{
throw EntityUtil.InvalidOwnerStateForAttach();
}
}
// verifies requirements for child entity passed to Attach()
internal protected void ValidateEntityForAttach(TEntity entity, int index, bool allowCollection)
{
if (null == entity)
{
if (allowCollection)
{
throw EntityUtil.InvalidNthElementNullForAttach(index);
}
else
{
throw EntityUtil.ArgumentNull("entity");
}
}
// verify the entity exists in the current context
Debug.Assert(null != this.ObjectContext,
"ObjectContext must not be null after call to ValidateOwnerForAttach");
Debug.Assert(!UsingNoTracking, "We should not be here for NoTracking case.");
ObjectStateEntry stateEntry = this.ObjectContext.ObjectStateManager.FindObjectStateEntry(entity);
if (null == stateEntry || !Object.ReferenceEquals(stateEntry.Entity, entity))
{
if (allowCollection)
{
throw EntityUtil.InvalidNthElementContextForAttach(index);
}
else
{
throw EntityUtil.InvalidEntityContextForAttach();
}
}
Debug.Assert(stateEntry.State != EntityState.Detached,
"State cannot be detached if the entry was retrieved from the context");
// verify the state of the entity (may not be in added state, since we only support attaching relationships
// to existing entities)
if (stateEntry.State != EntityState.Unchanged &&
stateEntry.State != EntityState.Modified)
{
if (allowCollection)
{
throw EntityUtil.InvalidNthElementStateForAttach(index);
}
else
{
throw EntityUtil.InvalidEntityStateForAttach();
}
}
}
internal abstract IEnumerable CreateSourceQueryInternal();
///
/// Adds an entity to the related end. If the owner is
/// attached to a cache then the all the connected ends are
/// added to the object cache and their corresponding relationships
/// are also added to the ObjectStateManager. The RelatedEnd of the
/// relationship is also fixed.
///
///
/// Entity instance of type IEntityWithRelationships to add to the related end
///
void IRelatedEnd.Add(IEntityWithRelationships entity)
{
EntityUtil.CheckArgumentNull(entity, "entity");
if (_owner != null)
{
Add(entity, /*applyConstraints*/true);
}
else
{
// The related end is in a disconnected state, so the related end is just a container
// A common scenario for this is during WCF deserialization
DisconnectedAdd(entity);
}
}
///
/// Removes an entity from the related end. If owner is
/// attached to a cache, marks relationship for deletion and if
/// the relationship is composition also marks the entity for deletion.
///
///
/// Entity instance of type IEntityWithRelationships to remove from the related end
///
/// Returns true if the entity was successfully removed, false if the entity was not part of the RelatedEnd.
bool IRelatedEnd.Remove(IEntityWithRelationships entity)
{
EntityUtil.CheckArgumentNull(entity, "entity");
if (_owner != null)
{
if (this.ContainsEntity(entity))
{
Remove(entity, /*fixup*/true, /*deleteEntity*/false, /*deleteOwner*/false, /*applyReferentialConstraints*/true);
return true;
}
// The entity is not related so return false
return false;
}
else
{
// The related end is in a disconncted state, so the related end is just a container
// A common scenario for this is during WCF deserialization
return DisconnectedRemove(entity);
}
}
internal abstract void DisconnectedAdd(IEntityWithRelationships entity);
internal abstract bool DisconnectedRemove(IEntityWithRelationships entity);
internal void Add(IEntityWithRelationships entity, bool applyConstraints)
{
// Verify that the entity and owner are in a valid state before we try to do anything with them
EntityUtil.ValidateRelationshipManager(entity);
EntityUtil.ValidateRelationshipManager(_owner);
// SQLBU: 508819 508813 508752
// Detect as soon as possible if we are trying to readd entities which are in Deleted state.
// When one of the entity is in Deleted state, attempt would be made to readd this entity
// to the OSM which is not allowed.
// NOTE: Current cleaning code (which uses cleanupOwnerEntity and cleanupPassedInEntity)
// works only if one of the entity is not attached to the context.
// PERFORMANCE: following can be performed faster if ObjectStateManager provide method to
// lookup only in dictionary with Deleted entities (because here we are interestede only in Deleted entities)
if (_context != null && !UsingNoTracking)
{
ValidateStateForAdd(_owner);
ValidateStateForAdd(entity);
}
this.Add(entity,
applyConstraints,
false /*addRelationshipAsUnchanged*/,
false /*relationshipAlreadyExists*/);
}
internal void CheckRelationEntitySet(EntitySet set)
{
Debug.Assert(set != null, "null EntitySet");
Debug.Assert(_relationshipSet != null,
"Should only be checking the RelationshipSet on an attached entity and it should always be non-null in that case");
if ((((AssociationSet)_relationshipSet).AssociationSetEnds[_navigation.To] != null) &&
(((AssociationSet)_relationshipSet).AssociationSetEnds[_navigation.To].EntitySet != set))
{
throw EntityUtil.EntitySetIsNotValidForRelationship(set.EntityContainer.Name, set.Name, _navigation.To, _relationshipSet.EntityContainer.Name, _relationshipSet.Name);
}
}
internal void ValidateStateForAdd(IEntityWithRelationships entity)
{
ObjectStateEntry entry = this.ObjectContext.ObjectStateManager.FindObjectStateEntry(entity);
if (entry != null && entry.State == EntityState.Deleted)
{
throw EntityUtil.ObjectStateManagerDoesnotAllowToReAddUnchangedOrModifiedOrDeletedEntity(EntityState.Deleted);
}
}
internal void Add(IEntityWithRelationships targetEntity, bool applyConstraints, bool addRelationshipAsUnchanged, bool relationshipAlreadyExists)
{
// Do verification
if (!this.VerifyEntityForAdd(targetEntity, relationshipAlreadyExists))
{
// Allow the same item to be "added" to a collection as a no-op operation
return;
}
EntityKey key = ObjectStateManager.FindKeyOnEntityWithRelationships(targetEntity);
if ((object)key != null && ObjectContext != null)
{
CheckRelationEntitySet(key.GetEntitySet(ObjectContext.MetadataWorkspace));
}
RelatedEnd targetRelatedEnd = GetOtherEndOfRelationship(targetEntity);
if (Object.ReferenceEquals(this.ObjectContext, targetRelatedEnd.ObjectContext) && this.ObjectContext != null)
{
// Both entities are associated with the same non-null context
// Make sure that they are either both tracked or both not tracked, or both don't have contexts
if (UsingNoTracking != targetRelatedEnd.UsingNoTracking)
{
throw EntityUtil.CannotCreateRelationshipBetweenTrackedAndNoTrackedEntities(UsingNoTracking ?
this._navigation.From : this._navigation.To);
}
}
else if(this.ObjectContext != null && targetRelatedEnd.ObjectContext != null)
{
// Both entities have a context
if (UsingNoTracking && targetRelatedEnd.UsingNoTracking)
{
// Both entities are NoTracking, but have different contexts
// Attach the owner's context to the target's RelationshipManager
// O-C mappings are 1:1, so this operation is allowed
targetEntity.RelationshipManager.ResetContext(this.ObjectContext, GetTargetEntitySetFromRelationshipSet(), MergeOption.NoTracking);
}
else
{
// Both entities are already tracked by different non-null contexts
throw EntityUtil.CannotCreateRelationshipEntitiesInDifferentContexts();
}
}
targetRelatedEnd.VerifyEntityForAdd(this.Owner, relationshipAlreadyExists);
// Do actual add
// Add the target entity to the source entity's collection or reference
this.AddEntityToLocallyCachedCollection(targetEntity, applyConstraints);
// Fix up the target end of the relationship by adding the source entity to the target entity's collection or reference
// devnote: applyConstraints should always be false in this fixup scenario,
// and should not just use the same value that was passed into this method.
targetRelatedEnd.AddEntityToLocallyCachedCollection(this.Owner, /*applyConstraints*/ false);
// delay event firing for targetRelatedEnd. once we fire the event, we should be at operation completed state
// Ensure that both entities end up in the same context:
// (1) If neither entity is attached to a context, we don't need to do anything else.
// (2) If they are both in the same one, we need to make sure neither one was created with MergeOption.NoTracking,
// and if not, add a relationship entry if it doesn't already exist.
// (3) If both entities are already in different contexts, fail.
// (4) Otherwise, only one entity is attached, and that is the context we will use.
// For the entity that is not attached, attach it to that context.
RelatedEnd attachedRelatedEnd = null; // the end of the relationship that is already attached to a context, if there is one.
IEntityWithRelationships entityToAdd = null; // the entity to be added to attachedRelatedEnd
HashSet promotedEntityKeyRefs = new HashSet();
if (Object.ReferenceEquals(this.ObjectContext, targetRelatedEnd.ObjectContext) && this.ObjectContext != null)
{
// Both entities are associated with the same non-null context
// Make sure that a relationship entry exists between these two entities. It is possible that the entities could
// have been added to the context independently of each other, so the relationship may not exist yet.
if (!relationshipAlreadyExists && !UsingNoTracking)
{
AddRelationshipToObjectStateManager(targetEntity, addRelationshipAsUnchanged, /*doAttach*/false);
}
}
else if (this.ObjectContext != null || targetRelatedEnd.ObjectContext != null)
{
// Only one entity has a context, so figure out which one it is, and determine which entity we will be adding to it
if (this.ObjectContext == null)
{
attachedRelatedEnd = targetRelatedEnd;
entityToAdd = this.Owner;
}
else
{
attachedRelatedEnd = this;
entityToAdd = targetEntity;
}
try
{
if (!attachedRelatedEnd.UsingNoTracking)
{
attachedRelatedEnd.AddGraphToObjectStateManager(entityToAdd, relationshipAlreadyExists,
addRelationshipAsUnchanged, /*doAttach*/ false, promotedEntityKeyRefs);
}
// Reset so we know we successfully added the graph and don't have to clean anything up
attachedRelatedEnd = null;
entityToAdd = null;
}
finally
{
// if attachedRelatedEnd is still set, that means we failed during the add to the state manager, and need to clean up
if (attachedRelatedEnd != null)
{
Debug.Assert(entityToAdd != null, "entityToAdd should be set if attachedRelatedEnd is set");
// Remove the source entity from the target related end
attachedRelatedEnd.FixupOtherEndOfRelationshipForRemove(entityToAdd);
// Remove the target entity from the source related end
attachedRelatedEnd.RemoveEntityFromLocallyCachedCollection(entityToAdd, /*resetIsLoaded*/ false);
// Remove the graph that we just tried to add to the context
RelationshipManager.RemoveRelatedEntitiesFromObjectStateManager(entityToAdd, promotedEntityKeyRefs);
Debug.Assert(promotedEntityKeyRefs.Count == 0, "Haven't cleaned up all of the promoted reference EntityKeys");
RemoveEntityFromObjectStateManager(entityToAdd);
}
}
}
// else neither entity is associated with a context, so there is no state manager to update
// fire the Association changed event, first on targetRelatedEnd then on this EC
targetRelatedEnd.OnAssociationChanged(CollectionChangeAction.Add, this.Owner);
OnAssociationChanged(CollectionChangeAction.Add, targetEntity);
}
private void AddGraphToObjectStateManager(IEntityWithRelationships entity, bool relationshipAlreadyExists,
bool addRelationshipAsUnchanged, bool doAttach, HashSet promotedEntityKeyRefs)
{
Debug.Assert(!UsingNoTracking, "Should not be attempting to add graphs to the state manager with NoTracking related ends");
AddEntityToObjectStateManager(entity, doAttach);
if (!relationshipAlreadyExists)
{
AddRelationshipToObjectStateManager(entity, addRelationshipAsUnchanged, doAttach);
}
WalkObjectGraphToIncludeAllRelatedEntities(entity, addRelationshipAsUnchanged, doAttach, promotedEntityKeyRefs);
}
internal void Remove(IEntityWithRelationships entity, bool doFixup, bool deleteEntity, bool deleteOwner, bool applyReferentialConstraints)
{
// Verify that the entity and owner are in a valid state before we try to do anything with them
EntityUtil.ValidateRelationshipManager(entity);
EntityUtil.ValidateRelationshipManager(_owner);
if (!this.ContainsEntity(entity))
{
return;
}
// There can be a case when symmetrical Remove() shall be performed because of Referential Constraints
// Example:
// Relationshipo Client -> Order with Referential Constraint on in.
// When user calls (pseudo code) Order.Remove(Client), we perform Client.Remove(Order),
// because removing relationship between Client and Order should cause cascade delete on the Order side.
if (null != _context && doFixup &&
applyReferentialConstraints &&
IsDependentEndOfReferentialConstraint())
{
// Remove _owner from the related end with applying Referential Constraints
RelatedEnd relatedEnd = GetOtherEndOfRelationship(entity);
relatedEnd.Remove(_owner, doFixup, deleteEntity, deleteOwner, applyReferentialConstraints);
return;
}
//The following call will verify that the given entity is part of the collection or ref.
bool fireEvent = RemoveEntityFromLocallyCachedCollection(entity, false);
if (!UsingNoTracking)
{
MarkRelationshipAsDeletedInObjectStateManager(entity, _owner, _relationshipSet, _navigation);
}
if (doFixup)
{
bool deleteRelatedEntity = false;
//The related end "entity" cannot live without this side "owner". It should be deleted. Cascade this
// effect to related enteties of the "related" entity
if (null != _context && (deleteEntity ||
(deleteOwner && CheckCascadeDeleteFlag(_fromEndProperty)) ||
(applyReferentialConstraints && IsPrincipalEndOfReferentialConstraint())))
{
//Once related entity is deleted, all relationships involving related entity would be updated
deleteRelatedEntity = true;
}
// RemoveEntityFromRelatedEnds check for graph circularities to make sure
// it does not get into infinite loop
if (deleteRelatedEntity)
{
RemoveEntityFromRelatedEnds(entity, _owner, _navigation.Reverse);
}
FixupOtherEndOfRelationshipForRemove(entity);
if (deleteRelatedEntity)
{
MarkEntityAsDeletedInObjectStateManager(entity);
}
}
if (fireEvent)
{
OnAssociationChanged(CollectionChangeAction.Remove, entity);
}
}
///
/// Check if current RelatedEnd is a Dependent end of some Referential Constraint
///
internal bool IsDependentEndOfReferentialConstraint()
{
if (null != _relationMetadata)
{
// NOTE Referential constraints collection will usually contains 0 or 1 element,
// so performance shouldn't be an issue here
foreach (ReferentialConstraint constraint in ((AssociationType)_relationMetadata).ReferentialConstraints)
{
if (constraint.ToRole == this._fromEndProperty)
{
// Example:
// Client --- Order
// RI Constraint: Principal/From , Dependent/To
// When current RelatedEnd is a CollectionOrReference in Order's relationships,
// constarint.ToRole == this._fromEndProperty == Order
return true;
}
}
}
return false;
}
///
/// Check if current RelatedEnd is a Principal end of some Referential Constraint
///
internal bool IsPrincipalEndOfReferentialConstraint()
{
if (null != _relationMetadata)
{
// NOTE Referential constraints collection will usually contains 0 or 1 element,
// so performance shouldn't be an issue here
foreach (ReferentialConstraint constraint in ((AssociationType)_relationMetadata).ReferentialConstraints)
{
if (constraint.FromRole == this._fromEndProperty)
{
// Example:
// Client --- Order
// RI Constraint: Principal/From , Dependent/To
// When current RelatedEnd is a CollectionOrReference in Client's relationships,
// constarint.FromRole == this._fromEndProperty == Client
return true;
}
}
}
return false;
}
//Add given entity and its relationship to ObjectStateManager. Walk graph to recursively
// add all entities in the graph.
// If doAttach==TRUE, the entities are attached directly as Unchanged without calling AcceptChanges()
internal void IncludeEntity(U entity, bool addRelationshipAsUnchanged, bool doAttach, HashSet promotedEntityKeyRefs)
where U : class, IEntityWithRelationships
{
Debug.Assert(!UsingNoTracking, "Should not be trying to include entities in the state manager for NoTracking related ends");
//check to see if entity is already added to the cache
//search by object reference so that we will not find any entries with the same key but a different object instance
// NOTE: if (cacheEntry.Entity == entity) then this part of the graph is skipped
ObjectStateEntry cacheEntry = _context.ObjectStateManager.FindObjectStateEntry(entity);
Debug.Assert(cacheEntry == null || cacheEntry.Entity == entity,
"Expected to have looked up this state entry by reference, how did we get a different entity?");
if (null == cacheEntry ||
cacheEntry.State == EntityState.Deleted)
{
// NOTE (Attach): if (null == entity.Key) then check must be performed whether entity really
// doesn't exist in the context (by creating fake Key and calling FindObjectStateEntry(Key) )
// This is done in the ObjectContext::AttachSingleObject().
AddGraphToObjectStateManager(entity, /*relationshipAlreadyExists*/ false,
addRelationshipAsUnchanged, doAttach, promotedEntityKeyRefs);
}
// There is a possibility that related entity is added to cache but relationship is not added.
// Example: Suppose A and B are related. When walking the graph it is possible that
// node B was visited through some relationship other than A-B.
else if (null == FindRelationshipEntryInObjectStateManager(entity))
{
// If we have a reference with a detached key, make sure the key matches the relationship we are about to add
EntityReference entityRef = this as EntityReference;
if (entityRef != null && entityRef.DetachedEntityKey != null)
{
EntityKey targetKey = _context.ObjectStateManager.GetEntityKey(entity);
if (entityRef.DetachedEntityKey != targetKey)
{
throw EntityUtil.EntityKeyValueMismatch();
}
// else -- null just means the key isn't set, so the target entity key doesn't also have to be null
}
AddRelationshipToObjectStateManager(entity, addRelationshipAsUnchanged, doAttach);
}
// else relationship is already there, nothing more to do
}
// Remove given entity and its relationship from ObjectStateManager.
// Traversegraph to recursively remove all entities in the graph.
internal void ExcludeEntity(U entity, HashSet promotedEntityKeyRefs)
where U : class, IEntityWithRelationships
{
Debug.Assert(!UsingNoTracking, "Should not try to exclude entities from the state manager for NoTracking related ends.");
//check to see if entity is already removed from the cache
ObjectStateEntry cacheEntry = _context.ObjectStateManager.FindObjectStateEntry(entity);
if (null != cacheEntry && cacheEntry.State != EntityState.Deleted && !entity.RelationshipManager.NodeVisited)
{
entity.RelationshipManager.NodeVisited = true;
RelationshipManager.RemoveRelatedEntitiesFromObjectStateManager(entity, promotedEntityKeyRefs);
RemoveRelationshipFromObjectStateManager(entity, _owner, _relationshipSet, _navigation);
RemoveEntityFromObjectStateManager(entity);
}
// There is a possibility that related entity is removed from cache but relationship is not removed.
// Example: Suppose A and B are related. When walking the graph it is possible that
// node B was visited through some relationship other than A-B.
else if (null != FindRelationshipEntryInObjectStateManager(entity))
{
RemoveRelationshipFromObjectStateManager(entity, _owner, _relationshipSet, _navigation);
}
}
internal ObjectStateEntry FindRelationshipEntryInObjectStateManager(IEntityWithRelationships entity)
{
Debug.Assert(!UsingNoTracking, "Should not look for RelationshipEntry in ObjectStateManager for NoTracking cases.");
EntityKey entityKey = ObjectContext.FindEntityKey(entity, this._context);
EntityKey ownerKey = ObjectContext.FindEntityKey(_owner, this._context);
return this._context.ObjectStateManager.FindRelationship(_relationshipSet,
new KeyValuePair(_navigation.From, ownerKey),
new KeyValuePair(_navigation.To, entityKey));
}
internal void Clear(IEntityWithRelationships entity, RelationshipNavigation navigation, bool doCascadeDelete)
{
ClearCollectionOrRef(entity, navigation, doCascadeDelete);
}
// Check if related entities contain proper property values
// (entities with temporary keys are skiped)
internal bool CheckReferentialConstraintProperties(EntityKey ownerKey)
{
// if the related end contains a real entity or is a reference with a detached entitykey, we need to check for RI constraints
if (!this.IsEmpty() ||
((ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne ||
ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One) &&
((EntityReference)this).DetachedEntityKey != null))
{
foreach (ReferentialConstraint constraint in ((AssociationType)this.RelationMetadata).ReferentialConstraints)
{
// Check properties in principals
if (constraint.ToRole == FromEndProperty)
{
if (!this.IsEmpty())
{
foreach (IEntityWithRelationships relatedEntity in this)
{
EntitySet principalEntitySet = GetTargetEntitySetFromRelationshipSet();
// get or create a key to use to compare the values -- the target entity might not have been attached
// yet so it may not have a key, but we can create one here to use for checking the values
EntityKey principalKey = ObjectContext.FindEntityKey(relatedEntity, this.ObjectContext);
if (null != (object)principalKey)
{
// Validate the key here because we need to get values from it for verification
// and that will fail if the key is malformed.
// Verify only if the key already exists.
EntityUtil.ValidateEntitySetInKey(principalKey, principalEntitySet);
principalKey.ValidateEntityKey(principalEntitySet);
}
else
{
principalKey = _context.CreateEntityKey(principalEntitySet, relatedEntity);
}
if (!VerifyRIConstraintsWithRelatedEntry(constraint, ownerKey, principalKey))
{
return false;
}
// else keep processing the other constraints
}
}
else
{
// related end is empty, so we must have a reference with a detached key
EntityKey detachedKey = ((EntityReference)this).DetachedEntityKey;
// don't need to validate the principal/detached key here because that has already been done during AttachContext
if (!VerifyRIConstraintsWithRelatedEntry(constraint, ownerKey, detachedKey))
{
return false;
}
// else keep processing the other constraints
}
}
else
// Check properties in dependent (only if the related end is an empty EntityReference with EntityKey)
if (constraint.FromRole == FromEndProperty && this.IsEmpty())
{
// related end is empty, so we must have a reference with a detached key
EntityKey detachedKey = ((EntityReference)this).DetachedEntityKey;
// don't need to validate the principal/detached key here because that has already been done during AttachContext
if (!VerifyRIConstraintsWithRelatedEntry(constraint, detachedKey, ownerKey))
{
return false;
}
// else keep processing the other constraints
}
}
}
return true;
}
private static bool VerifyRIConstraintsWithRelatedEntry(ReferentialConstraint constraint, EntityKey dependentKey, EntityKey principalKey)
{
Debug.Assert(constraint.FromProperties.Count == constraint.ToProperties.Count, "RIC: Referential constraints From/To properties list have different size");
// NOTE order of properties in collections (From/ToProperties) is important.
for (int i = 0; i < constraint.FromProperties.Count; ++i)
{
string fromPropertyName = constraint.FromProperties[i].Name;
string toPropertyName = constraint.ToProperties[i].Name;
object currentValue = principalKey.FindValueByName(fromPropertyName);
object expectedValue = dependentKey.FindValueByName(toPropertyName);
Debug.Assert(currentValue != null, "currentValue is part of Key on an attached entity, it must not be null");
if (!currentValue.Equals(expectedValue))
{
// RI Constraint violated
return false;
}
}
return true;
}
public IEnumerator GetEnumerator()
{
// Due to the way the CLR handles IEnumerator return types, the check for a null owner for EntityReferences
// must be made here because GetInternalEnumerator is delay-executed and so will not throw until the
// enumerator is advanced
if (this is EntityReference)
{
CheckOwnerNull();
}
return GetInternalEnumerator();
}
internal void RemoveAll()
{
//copy into list because changing collection member is not allowed during enumeration.
// If possible avoid copying into list.
List deletedEntities = null;
bool fireEvent = false;
try
{
_suppressEvents = true;
foreach (IEntityWithRelationships e in this)
{
if (null == deletedEntities)
{
deletedEntities = new List();
}
deletedEntities.Add(e);
}
if (fireEvent = (null != deletedEntities) && (deletedEntities.Count > 0))
{
foreach (IEntityWithRelationships e in deletedEntities)
{
Remove(e, /*fixup*/true, /*deleteEntity*/false, /*deleteOwner*/true, /*applyReferentialConstraints*/true);
}
}
}
finally
{
_suppressEvents = false;
}
if (fireEvent)
{
OnAssociationChanged(CollectionChangeAction.Refresh, null);
}
}
internal void DetachAll(EntityState ownerEntityState)
{
//copy into list because changing collection member is not allowed during enumeration.
// If possible avoid copying into list.
List deletedEntities = new List();
foreach (IEntityWithRelationships e in this)
{
deletedEntities.Add(e);
}
bool detachRelationship =
ownerEntityState == EntityState.Added ||
_fromEndProperty.RelationshipMultiplicity == RelationshipMultiplicity.Many;
// every-fix up will fire with Remove action
// every forward operation (removing from this relatedEnd) will fire with Refresh
// do not merge the loops, handle the related ends seperately (when the event is being fired,
// we should be in good state: for every entity deleted, related event should have been fired)
foreach (IEntityWithRelationships entity in deletedEntities)
{
// future enhancement: it does not make sense to return in the half way, either remove this code or
// move it to the right place
if (!this.ContainsEntity(entity))
{
return;
}
// if this is a reference, set the EntityKey property before removing the relationship and entity
EntityReference entityReference = this as EntityReference;
if (entityReference != null)
{
entityReference.DetachedEntityKey = entityReference.AttachedEntityKey;
}
if (detachRelationship)
{
DetachRelationshipFromObjectStateManager(entity, _owner, _relationshipSet, _navigation);
}
RelatedEnd relatedEnd = GetOtherEndOfRelationship(entity);
relatedEnd.RemoveEntityFromLocallyCachedCollection(_owner, /* resetIsLoaded */ true);
relatedEnd.OnAssociationChanged(CollectionChangeAction.Remove, _owner);
}
foreach (IEntityWithRelationships entity in deletedEntities)
{
RelatedEnd relatedEnd = GetOtherEndOfRelationship(entity);
this.RemoveEntityFromLocallyCachedCollection(entity, /* resetIsLoaded */ false);
}
this.OnAssociationChanged(CollectionChangeAction.Refresh, null);
Debug.Assert(this.IsEmpty(), "Collection or reference should be empty");
}
internal abstract bool VerifyEntityForAdd(IEntityWithRelationships entity, bool relationshipAlreadyExists);
internal abstract void AddEntityToLocallyCachedCollection(IEntityWithRelationships entity, bool applyConstraints);
internal abstract bool RemoveEntityFromLocallyCachedCollection(IEntityWithRelationships entity, bool resetIsLoaded);
internal abstract void Include(bool addRelationshipAsUnchanged, bool doAttach, HashSet promotedEntityKeyRefs);
internal abstract void Exclude(HashSet promotedEntityKeyRefs);
internal abstract void ClearCollectionOrRef(IEntityWithRelationships entity, RelationshipNavigation navigation, bool doCascadeDelete);
internal abstract bool ContainsEntity(IEntityWithRelationships entity);
internal abstract IEnumerator GetInternalEnumerator();
internal abstract void RetrieveReferentialConstraintProperties(Dictionary> keyValues, HashSet