//----------------------------------------------------------------------
//
// 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.Metadata.Edm;
using System.Diagnostics;
using System.Runtime.Serialization;
namespace System.Data.Objects.DataClasses
{
///
/// Models a relationship end with multiplicity 1.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
[DataContract]
[Serializable]
public sealed class EntityReference : EntityReference
where TEntity : class, IEntityWithRelationships
{
// ------
// Fields
// ------
// 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.
private TEntity _cachedValue;
// ------------
// Constructors
// ------------
///
/// The default constructor is required for some serialization scenarios. It should not be used to
/// create new EntityReferences. Use the GetRelatedReference or GetRelatedEnd methods on the RelationshipManager
/// class instead.
///
public EntityReference()
{
}
internal EntityReference(IEntityWithRelationships owner, RelationshipNavigation navigation, IRelationshipFixer relationshipFixer)
: base(owner, navigation, relationshipFixer)
{
this._cachedValue = null;
}
// ----------
// Properties
// ----------
///
/// Stub only please replace with actual implementation
///
[System.Xml.Serialization.SoapIgnore]
[System.Xml.Serialization.XmlIgnore]
public TEntity Value
{
get
{
CheckOwnerNull();
return _cachedValue;
}
set
{
CheckOwnerNull();
//setting to same value is a no-op (SQLBU DT # 446320)
//setting to null is a special case because then we will also clear out any Added/Unchanged relationships with key entries, so we can't no-op if Value is null
if(value != null && value == _cachedValue)
{
return;
}
ValidateOwnerWithRIConstraints();
if (null != value)
{
Add(value, /*applyConstraints*/false);
}
else
{
if (UsingNoTracking)
{
if (_cachedValue != null)
{
// The other end of relationship can be the EntityReference or EntityCollection
// If the other end is EntityReference, its IsLoaded property should be set to FALSE
RelatedEnd relatedEnd = GetOtherEndOfRelationship(_cachedValue);
relatedEnd.OnRelatedEndClear();
}
_isLoaded = false;
}
ClearCollectionOrRef(null, null, false);
}
}
}
internal override object CachedValue
{
get { return _cachedValue; }
}
internal override object ReferenceValue
{
get
{
return Value;
}
set
{
Value = (TEntity)value;
}
}
// -------
// Methods
// -------
///
/// Loads the related entity or entities into the local related end using the supplied MergeOption.
///
public override void Load(MergeOption mergeOption)
{
CheckOwnerNull();
// Validate that the Load is possible
ObjectQuery sourceQuery = ValidateLoad(mergeOption, "EntityReference");
_suppressEvents = true; // we do not want any event during the bulk operation
try
{
List refreshedValue = new List(GetResults(sourceQuery));
if (refreshedValue.Count > 1)
{
throw EntityUtil.MoreThanExpectedRelatedEntitiesFound();
}
if (refreshedValue.Count == 0)
{
if (ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One)
{
//query returned zero related end; one related end was expected.
throw EntityUtil.LessThanExpectedRelatedEntitiesFound();
}
else if (mergeOption == MergeOption.OverwriteChanges || mergeOption == MergeOption.PreserveChanges)
{
// This entity is not related to anything in this AssociationSet and Role on the server.
// If there is an existing _cachedValue, we may need to clear it out, based on the MergeOption
EntityKey sourceKey = ObjectStateManager.FindKeyOnEntityWithRelationships(Owner);
EntityUtil.CheckEntityKeyNull(sourceKey);
ObjectStateManager.RemoveRelationships(ObjectContext, mergeOption, (AssociationSet)RelationshipSet, sourceKey, (AssociationEndMember)FromEndProperty);
}
// else this is NoTracking or AppendOnly, and no entity was retrieved by the Load, so there's nothing extra to do
// Since we have no value and are not doing a merge, the last step is to set IsLoaded to true
_isLoaded = true;
}
else
{
Merge(refreshedValue, mergeOption, true /*setIsLoaded*/);
}
}
finally
{
_suppressEvents = false;
}
// fire the AssociationChange with Refresh
OnAssociationChanged(CollectionChangeAction.Refresh, null);
}
///
/// This operation is not allowed if the owner is null
///
///
internal override IEnumerator GetInternalEnumerator()
{
if(Value != null)
{
yield return Value;
}
}
///
/// Attaches an entity to the EntityReference. 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 EntityCollection
/// Thrown when is null.
/// Thrown when the entity cannot be related via the current relationship end.
public void Attach(TEntity entity)
{
CheckOwnerNull();
((IRelatedEnd)this).Attach(entity);
}
internal override void Include(bool addRelationshipAsUnchanged, bool doAttach, HashSet promotedEntityKeyRefs)
{
Debug.Assert(this.ObjectContext != null, "Should not be trying to add entities to state manager if context is null");
// If we have an actual value or a key for this reference, add it to the context
if (null != _cachedValue)
{
IncludeEntity(_cachedValue, addRelationshipAsUnchanged, doAttach, promotedEntityKeyRefs);
}
else if (DetachedEntityKey != null)
{
IncludeEntityKey(doAttach, promotedEntityKeyRefs);
}
// else there is nothing to add for this relationship
}
private void IncludeEntityKey(bool doAttach, HashSet promotedEntityKeyRefs)
{
ObjectStateManager manager = this.ObjectContext.ObjectStateManager;
bool addNewRelationship = false;
bool addKeyEntry = false;
ObjectStateEntry existingEntry = manager.FindObjectStateEntry(DetachedEntityKey);
if (existingEntry == null)
{
// add new key entry and create a relationship with it
addKeyEntry = true;
addNewRelationship = true;
}
else
{
if (existingEntry.IsKeyEntry)
{
// We have an existing key entry, so just need to add a relationship with it
// We know the target end of this relationship is 1..1 or 0..1 since it is a reference, so if the source end is also not Many, we have a 1-to-1
if (FromEndProperty.RelationshipMultiplicity != RelationshipMultiplicity.Many)
{
// before we add a new relationship to this key entry, make sure it's not already related to something else
// We have to explicitly do this here because there are no other checks to make sure a key entry in a 1-to-1 doesn't end up in two of the same relationship
foreach (ObjectStateEntry relationshipEntry in this.ObjectContext.ObjectStateManager.FindRelationshipsByKey(DetachedEntityKey))
{
// only care about relationships in the same AssociationSet and where the key is playing the same role that it plays in this EntityReference
if (relationshipEntry.IsSameAssociationSetAndRole((AssociationSet)RelationshipSet, DetachedEntityKey, (AssociationEndMember)ToEndMember) &&
relationshipEntry.State != EntityState.Deleted)
{
throw EntityUtil.EntityConflictsWithKeyEntry();
}
}
}
addNewRelationship = true;
}
else
{
// since we verified that the EntitySet is correct, the type of existingEntry.Entity must
// be correct and therefore it has to be an IEntityWithRelationships, so just cast
IEntityWithRelationships targetEntity = (IEntityWithRelationships)(existingEntry.Entity);
// Verify that the target entity is in a valid state for adding a relationship
if (existingEntry.State == EntityState.Deleted)
{
throw EntityUtil.ObjectStateManagerDoesnotAllowToReAddUnchangedOrModifiedOrDeletedEntity(EntityState.Deleted);
}
// We know the target end of this relationship is 1..1 or 0..1 since it is a reference, so if the source end is also not Many, we have a 1-to-1
RelatedEnd relatedEnd = (RelatedEnd)targetEntity.RelationshipManager.GetRelatedEnd(RelationshipName, RelationshipNavigation.From);
if (FromEndProperty.RelationshipMultiplicity != RelationshipMultiplicity.Many && !relatedEnd.IsEmpty())
{
// Make sure the target entity is not already related to something else.
// devnote: The call to Add below does *not* do this check for the fixup case, so if it's not done here, no failure will occur
// and existing relationships may be deleted unexpectedly. RelatedEnd.Include should not remove existing relationships, only add new ones.
throw EntityUtil.EntityConflictsWithKeyEntry();
}
// We have an existing entity with the same key, just hook up the related ends
this.Add(targetEntity,
true /*applyConstraints*/,
doAttach /*addRelationshipAsUnchanged*/,
false /*relationshipAlreadyExists*/);
// add to the list of promoted key references so we can cleanup if a failure occurs later
promotedEntityKeyRefs.Add(this);
}
}
if (addNewRelationship)
{
// devnote: If we add any validation here, it needs to go here before adding the key entry,
// otherwise we have to clean up that entry if the validation fails
if (addKeyEntry)
{
EntitySet targetEntitySet = DetachedEntityKey.GetEntitySet(this.ObjectContext.MetadataWorkspace);
manager.AddKeyEntry(DetachedEntityKey, targetEntitySet);
}
EntityKey ownerKey = ObjectContext.FindEntityKey(this.Owner, this.ObjectContext);
RelationshipWrapper wrapper = new RelationshipWrapper((AssociationSet)RelationshipSet,
RelationshipNavigation.From, ownerKey, RelationshipNavigation.To, DetachedEntityKey);
manager.AddNewRelation(wrapper, doAttach ? EntityState.Unchanged : EntityState.Added);
}
}
internal override void Exclude(HashSet promotedEntityKeyRefs)
{
Debug.Assert(this.ObjectContext != null, "Should not be trying to remove entities from state manager if context is null");
if (null != _cachedValue)
{
// It is possible that _cachedValue was originally null in this graph, but was only set
// while the graph was being added, if the DetachedEntityKey matched its key. In that case,
// we only want to clear _cachedValue and delete the relationship entry, but not remove the entity
// itself from the context.
if (promotedEntityKeyRefs.Contains(this))
{
// Retrieve the relationship entry before _cachedValue is set to null during Remove
ObjectStateEntry relationshipEntry = FindRelationshipEntryInObjectStateManager(_cachedValue);
Debug.Assert(relationshipEntry != null, "Should have been able to find a valid relationship since _cachedValue is non-null");
// Remove the related ends and mark the relationship as deleted, but don't propagate the changes to the target entity itself
Remove(_cachedValue, /*doFixup*/ true, /*deleteEntity*/ false, /*deleteOwner*/ false, /*applyRefentialConstraints*/ false);
// The relationship will now either be detached (if it was previously in the Added state), or Deleted (if it was previously Unchanged)
// If it's Deleted, we need to AcceptChanges to get rid of it completely
if (relationshipEntry.State != EntityState.Detached)
{
relationshipEntry.AcceptChanges();
}
// Since this has been processed, remove it from the list
promotedEntityKeyRefs.Remove(this);
}
else
{
ExcludeEntity(_cachedValue, promotedEntityKeyRefs);
}
}
else if (DetachedEntityKey != null)
{
// there may still be relationship entries with stubs that need to be removed
// this works whether we just added the key entry along with the relationship or if it was already existing
ExcludeEntityKey();
}
// else there is nothing to remove for this relationship
}
private void ExcludeEntityKey()
{
EntityKey ownerKey = ObjectContext.FindEntityKey(this.Owner, this.ObjectContext);
ObjectStateEntry relationshipEntry = this.ObjectContext.ObjectStateManager.FindRelationship(RelationshipSet,
new KeyValuePair(RelationshipNavigation.From, ownerKey),
new KeyValuePair(RelationshipNavigation.To, DetachedEntityKey));
// we may have failed in adding the graph before we actually added this relationship, so make sure we actually found one
if (relationshipEntry != null)
{
relationshipEntry.Delete(/*doFixup*/ false);
// If entry was Added before, it is now Detached, otherwise AcceptChanges to detach it
if (relationshipEntry.State != EntityState.Detached)
{
relationshipEntry.AcceptChanges();
}
}
}
internal override void ClearCollectionOrRef(IEntityWithRelationships entity, RelationshipNavigation navigation, bool doCascadeDelete)
{
if (null != _cachedValue)
{
// Following condition checks if we have already visited this graph node. If its true then
// we should not do fixup because that would cause circular loop
if ((entity == _cachedValue) && (navigation.Equals(this.RelationshipNavigation)))
{
Remove(_cachedValue, /*fixup*/false, /*deleteEntity*/false, /*deleteOwner*/false, /*applyReferentialConstraints*/false);
}
else
{
Remove(_cachedValue, /*fixup*/true, doCascadeDelete, /*deleteOwner*/false, /*applyReferentialConstraints*/true);
}
}
else
{
// this entity reference could be replacing a relationship that points to a key entry
// we need to search relationships on the Owner entity to see if this is true, and if so remove the relationship entry
if (Owner != null && Owner.RelationshipManager.Context != null && !UsingNoTracking)
{
ObjectStateEntry ownerEntry = Owner.RelationshipManager.Context.ObjectStateManager.GetObjectStateEntry(Owner);
ownerEntry.DeleteRelationshipsThatReferenceKeys(this.RelationshipSet, this.ToEndMember);
}
}
// If we have an Owner, clear the DetachedEntityKey.
// If we do not have an owner, retain the key so that we can resolve the difference when the entity is attached to a context
if (this.Owner != null)
{
// Clear the detachedEntityKey as well. In cases where we have to fix up the detachedEntityKey, we will not always be able to detect
// if we have *only* a Deleted relationship for a given entity/relationship/role, so clearing this here will ensure that
// even if no other relationships are added, the key value will still be correct.
((EntityReference)this).DetachedEntityKey = null;
}
}
///
///
///
///
///
/// True if the verify succeeded, False if the Add should no-op
internal override bool VerifyEntityForAdd(IEntityWithRelationships entity, bool relationshipAlreadyExists)
{
if (!relationshipAlreadyExists && this.ContainsEntity(entity))
{
return false;
}
if (!(entity is TEntity))
{
throw EntityUtil.InvalidContainedTypeReference(entity.GetType().FullName, typeof(TEntity).FullName);
}
return true;
}
//AddEntityToLocallyCachedCollection is used by both APIs a) IRelatedEnd.Add b) Value property setter.
// ApplyConstraints is true in case of IRelatedEnd.Add because one cannot add entity to ref it its already set
// however applyConstraints is false in case of Value property setter because value can be set to a new value
// even if its non null.
internal override void AddEntityToLocallyCachedCollection(IEntityWithRelationships entity, bool applyConstraints)
{
if (applyConstraints && null != _cachedValue)
{
throw EntityUtil.CannotAddMoreThanOneEntityToEntityReference();
}
ClearCollectionOrRef(null, null, false);
this._cachedValue = (TEntity)entity;
}
///
/// Disconnected adds are not supported for an EntityReference so we should report this as an error.
///
/// The entity to add to the related end in a disconnected state.
internal override void DisconnectedAdd(IEntityWithRelationships entity)
{
CheckOwnerNull();
}
///
/// Disconnected removes are not supported for an EntityReference so we should report this as an error.
///
/// The entity to remove from the related end in a disconnected state.
internal override bool DisconnectedRemove(IEntityWithRelationships entity)
{
CheckOwnerNull();
return false;
}
internal override bool RemoveEntityFromLocallyCachedCollection(IEntityWithRelationships entity, bool resetIsLoaded)
{
if (null != _cachedValue && entity != _cachedValue)
{
throw EntityUtil.EntityIsNotPartOfRelationship();
}
this._cachedValue = null;
if (resetIsLoaded)
_isLoaded = false;
return true;
}
// Method used to retrieve properties from principal entities.
// NOTE: 'properties' list is modified in this method and may already contains some properties.
internal override void RetrieveReferentialConstraintProperties(Dictionary> properties, HashSet