//---------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//
// @owner Microsoft
// @backupOwner Microsoft
//---------------------------------------------------------------------
namespace System.Data.Objects.DataClasses
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Data.Metadata.Edm;
using System.Data.Objects.Internal;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Serialization;
///
/// Collection of entities modeling a particular EDM construct
/// which can either be all entities of a particular type or
/// entities participating in a particular relationship.
///
[Serializable]
public sealed class EntityCollection : RelatedEnd, ICollection, IListSource
where TEntity : class
{
// ------
// Fields
// ------
// The following field is 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.
// Note that this field should no longer be used directly. Instead, use the _wrappedRelatedEntities
// field. This field is retained only for compatibility with the serialization format introduced in v1.
private HashSet _relatedEntities;
[NonSerialized]
private CollectionChangeEventHandler _onAssociationChangedforObjectView;
[NonSerialized]
private Dictionary _wrappedRelatedEntities;
// ------------
// Constructors
// ------------
///
/// Creates an empty EntityCollection.
///
public EntityCollection()
: base()
{
}
internal EntityCollection(IEntityWrapper wrappedOwner, RelationshipNavigation navigation, IRelationshipFixer relationshipFixer)
: base(wrappedOwner, navigation, relationshipFixer)
{
}
// ---------
// Events
// ---------
///
/// internal Event to notify changes in the collection.
///
// Dev notes -2
// following statement is valid on current existing CLR:
// lets say Customer is an Entity, Array[Customer] is not Array[Entity]; it is not supported
// to do the work around we have to use a non-Generic interface/class so we can pass the EntityCollection
// around safely (as RelatedEnd) without losing it.
// Dev notes -3
// this event is only used for internal purposes, to make sure views are updated before we fire public AssociationChanged event
internal override event CollectionChangeEventHandler AssociationChangedForObjectView
{
add
{
_onAssociationChangedforObjectView += value;
}
remove
{
_onAssociationChangedforObjectView -= value;
}
}
// ---------
// Properties
// ---------
private Dictionary WrappedRelatedEntities
{
get
{
if (null == _wrappedRelatedEntities)
{
_wrappedRelatedEntities = new Dictionary();
}
return _wrappedRelatedEntities;
}
}
// ----------------------
// ICollection Properties
// ----------------------
///
/// Count of entities in the collection.
///
public int Count
{
get
{
DeferredLoad();
return CountInternal;
}
}
internal int CountInternal
{
get
{
// count should not cause allocation
return ((null != _wrappedRelatedEntities) ? _wrappedRelatedEntities.Count : 0);
}
}
///
/// Whether or not the collection is read-only.
///
public bool IsReadOnly
{
get
{
return false;
}
}
// ----------------------
// IListSource Properties
// ----------------------
///
/// IListSource.ContainsListCollection implementation. Always returns true
///
bool IListSource.ContainsListCollection
{
get
{
return false; // this means that the IList we return is the one which contains our actual data, it is not a collection
}
}
// -------
// Methods
// -------
internal override void OnAssociationChanged(CollectionChangeAction collectionChangeAction, object entity)
{
Debug.Assert(!(entity is IEntityWrapper), "Object is an IEntityWrapper instance instead of the raw entity.");
if (!_suppressEvents)
{
if (_onAssociationChangedforObjectView != null)
{
_onAssociationChangedforObjectView(this, (new CollectionChangeEventArgs(collectionChangeAction, entity)));
}
if (_onAssociationChanged != null)
{
_onAssociationChanged(this, (new CollectionChangeEventArgs(collectionChangeAction, entity)));
}
}
}
// ----------------------
// IListSource method
// ----------------------
///
/// IListSource.GetList implementation
///
///
/// IList interface over the data to bind
///
IList IListSource.GetList()
{
EntityType rootEntityType = null;
if (WrappedOwner.Entity != null)
{
EntitySet singleEntitySet = null;
// if the collection is attached, we can use metadata information; otherwise, it is unavailable
if (null != this.RelationshipSet)
{
singleEntitySet = ((AssociationSet)this.RelationshipSet).AssociationSetEnds[this.ToEndMember.Name].EntitySet;
EntityType associationEndType = (EntityType)((RefType)((AssociationEndMember)this.ToEndMember).TypeUsage.EdmType).ElementType;
EntityType entitySetType = singleEntitySet.ElementType;
// the type is constrained to be either the entitySet.ElementType or the end member type, whichever is most derived
if (associationEndType.IsAssignableFrom(entitySetType))
{
// entity set exposes a subtype of the association
rootEntityType = entitySetType;
}
else
{
// use the end type otherwise
rootEntityType = associationEndType;
}
}
}
return ObjectViewFactory.CreateViewForEntityCollection(rootEntityType, this);
}
///
/// Loads the related entity or entities into the local collection using the supplied MergeOption.
/// Do merge if collection was already filled
///
public override void Load(MergeOption mergeOption)
{
CheckOwnerNull();
//Pass in null to indicate the CreateSourceQuery method should be used.
Load((List)null, mergeOption);
// do not fire the AssociationChanged event here,
// once it is fired in one level deeper, (at Internal void Load(IEnumerable)), you don't need to add the event at other
// API that call (Internal void Load(IEnumerable))
}
///
/// Loads related entities into the local collection. If the collection is already filled
/// or partially filled, merges existing entities with the given entities. The given
/// entities are not assumed to be the complete set of related entities.
///
/// Owner and all entities passed in must be in Unchanged or Modified state. We allow
/// deleted elements only when the state manager is already tracking the relationship
/// instance.
///
/// Result of query returning related entities
/// Thrown when is null.
/// Thrown when an entity in the given
/// collection cannot be related via the current relationship end.
public void Attach(IEnumerable entities)
{
EntityUtil.CheckArgumentNull(entities, "entities");
CheckOwnerNull();
//
IList wrappedEntities = new List();
foreach (TEntity entity in entities)
{
wrappedEntities.Add(EntityWrapperFactory.WrapEntityUsingContext(entity, ObjectContext));
}
Attach(wrappedEntities, true);
}
///
/// Attaches an entity to the EntityCollection. If the EntityCollection 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 EntityCollection
/// Thrown when is null.
/// Thrown when the entity cannot be related via the current relationship end.
public void Attach(TEntity entity)
{
EntityUtil.CheckArgumentNull(entity, "entity");
Attach(new IEntityWrapper[] { EntityWrapperFactory.WrapEntityUsingContext(entity, ObjectContext) }, false);
}
///
/// Requires: collection is null or contains related entities.
/// Loads related entities into the local collection.
///
/// If null, retrieves entities from the server through a query;
/// otherwise, loads the given collection
///
internal void Load(List collection, MergeOption mergeOption)
{
// Validate that the Load is possible
bool hasResults;
ObjectQuery sourceQuery = ValidateLoad(mergeOption, "EntityCollection", out hasResults);
// we do not want any Add or Remove event to be fired during Merge, we will fire a Refresh event at the end if everything is successful
_suppressEvents = true;
try
{
if (collection == null)
{
Merge(hasResults
? GetResults(sourceQuery)
: Enumerable.Empty(), mergeOption, true /*setIsLoaded*/);
}
else
{
Merge(collection, mergeOption, true /*setIsLoaded*/);
}
}
finally
{
_suppressEvents = false;
}
// fire the AssociationChange with Refresh
OnAssociationChanged(CollectionChangeAction.Refresh, null);
}
///
///
///
public void Add(TEntity entity)
{
EntityUtil.CheckArgumentNull(entity, "entity");
Add(EntityWrapperFactory.WrapEntityUsingContext(entity, ObjectContext));
}
///
/// Add the item to the underlying collection
///
///
internal override void DisconnectedAdd(IEntityWrapper wrappedEntity)
{
Debug.Assert(wrappedEntity != null, "IEntityWrapper instance is null.");
// Validate that the incoming entity is also detached
if (null != wrappedEntity.Context && wrappedEntity.MergeOption != MergeOption.NoTracking)
{
throw EntityUtil.UnableToAddToDisconnectedRelatedEnd();
}
VerifyType(wrappedEntity);
// Add the entity to local collection without doing any fixup
AddToCache(wrappedEntity, /* applyConstraints */ false);
OnAssociationChanged(CollectionChangeAction.Add, wrappedEntity.Entity);
}
///
/// Remove the item from the underlying collection
///
///
///
internal override bool DisconnectedRemove(IEntityWrapper wrappedEntity)
{
Debug.Assert(wrappedEntity != null, "IEntityWrapper instance is null.");
// Validate that the incoming entity is also detached
if (null != wrappedEntity.Context && wrappedEntity.MergeOption != MergeOption.NoTracking)
{
throw EntityUtil.UnableToRemoveFromDisconnectedRelatedEnd();
}
// Remove the entity to local collection without doing any fixup
bool result = RemoveFromCache(wrappedEntity, /* resetIsLoaded*/ false, /*preserveForeignKey*/ false);
OnAssociationChanged(CollectionChangeAction.Remove, wrappedEntity.Entity);
return result;
}
///
/// Removes an entity from the EntityCollection. If the owner is
/// attached to a context, Remove marks the relationship for deletion and if
/// the relationship is composition also marks the entity for deletion.
///
///
/// Entity instance to remove from the EntityCollection
///
/// Returns true if the entity was successfully removed, false if the entity was not part of the RelatedEnd.
public bool Remove(TEntity entity)
{
EntityUtil.CheckArgumentNull(entity, "entity");
DeferredLoad();
return RemoveInternal(entity);
}
internal bool RemoveInternal(TEntity entity)
{
return Remove(EntityWrapperFactory.WrapEntityUsingContext(entity, ObjectContext), /*preserveForeignKey*/false);
}
internal override void Include(bool addRelationshipAsUnchanged, bool doAttach)
{
if (null != _wrappedRelatedEntities && null != this.ObjectContext)
{
List wrappedRelatedEntities = new List(_wrappedRelatedEntities.Values);
foreach (IEntityWrapper wrappedEntity in wrappedRelatedEntities)
{
// Sometimes with mixed POCO and IPOCO, you can get different instances of IEntityWrappers stored in the IPOCO related ends
// These should be replaced by the IEntityWrapper that is stored in the context
IEntityWrapper identityWrapper = EntityWrapperFactory.WrapEntityUsingContext(wrappedEntity.Entity, WrappedOwner.Context);
if (identityWrapper != wrappedEntity)
{
_wrappedRelatedEntities[(TEntity)identityWrapper.Entity] = identityWrapper;
}
IncludeEntity(identityWrapper, addRelationshipAsUnchanged, doAttach);
}
}
}
internal override void Exclude()
{
if (null != _wrappedRelatedEntities && null != this.ObjectContext)
{
if (!IsForeignKey)
{
foreach (IEntityWrapper wrappedEntity in _wrappedRelatedEntities.Values)
{
ExcludeEntity(wrappedEntity);
}
}
else
{
TransactionManager tm = ObjectContext.ObjectStateManager.TransactionManager;
Debug.Assert(tm.IsAddTracking || tm.IsAttachTracking, "Exclude being called while not part of attach/add rollback--PromotedEntityKeyRefs will be null.");
List values = new List(_wrappedRelatedEntities.Values);
foreach (IEntityWrapper wrappedEntity in values)
{
EntityReference otherEnd = GetOtherEndOfRelationship(wrappedEntity) as EntityReference;
Debug.Assert(otherEnd != null, "Other end of FK from a collection should be a reference.");
bool doFullRemove = tm.PopulatedEntityReferences.Contains(otherEnd);
bool doRelatedEndRemove = tm.AlignedEntityReferences.Contains(otherEnd);
if (doFullRemove || doRelatedEndRemove)
{
// Remove the related ends and mark the relationship as deleted, but don't propagate the changes to the target entity itself
otherEnd.Remove(otherEnd.CachedValue,
doFixup: doFullRemove,
deleteEntity: false,
deleteOwner: false,
applyReferentialConstraints: false,
preserveForeignKey: true);
// Since this has been processed, remove it from the list
if (doFullRemove)
{
tm.PopulatedEntityReferences.Remove(otherEnd);
}
else
{
tm.AlignedEntityReferences.Remove(otherEnd);
}
}
else
{
ExcludeEntity(wrappedEntity);
}
}
}
}
}
internal override void ClearCollectionOrRef(IEntityWrapper wrappedEntity, RelationshipNavigation navigation, bool doCascadeDelete)
{
if (null != _wrappedRelatedEntities)
{
//copy into list because changing collection member is not allowed during enumeration.
// If possible avoid copying into list.
List tempCopy = new List(_wrappedRelatedEntities.Values);
foreach (IEntityWrapper wrappedCurrent in tempCopy)
{
// 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 ((wrappedEntity.Entity == wrappedCurrent.Entity) && (navigation.Equals(RelationshipNavigation)))
{
Remove(wrappedCurrent, /*fixup*/false, /*deleteEntity*/false, /*deleteOwner*/false, /*applyReferentialConstraints*/false, /*preserveForeignKey*/false);
}
else
{
Remove(wrappedCurrent, /*fixup*/true, doCascadeDelete, /*deleteOwner*/false, /*applyReferentialConstraints*/false, /*preserveForeignKey*/false);
}
}
Debug.Assert(_wrappedRelatedEntities.Count == 0, "After removing all related entities local collection count should be zero");
}
}
internal override void ClearWrappedValues()
{
if (_wrappedRelatedEntities != null)
{
this._wrappedRelatedEntities.Clear();
}
if (_relatedEntities != null)
{
this._relatedEntities.Clear();
}
}
///
///
///
///
///
/// True if the verify succeeded, False if the Add should no-op
internal override bool VerifyEntityForAdd(IEntityWrapper wrappedEntity, bool relationshipAlreadyExists)
{
Debug.Assert(wrappedEntity != null, "IEntityWrapper instance is null.");
if (!relationshipAlreadyExists && this.ContainsEntity(wrappedEntity))
{
return false;
}
this.VerifyType(wrappedEntity);
return true;
}
internal override bool CanSetEntityType(IEntityWrapper wrappedEntity)
{
return wrappedEntity.Entity is TEntity;
}
internal override void VerifyType(IEntityWrapper wrappedEntity)
{
if (!CanSetEntityType(wrappedEntity))
{
throw EntityUtil.InvalidContainedTypeCollection(wrappedEntity.Entity.GetType().FullName, typeof(TEntity).FullName);
}
}
///
/// Remove from the RelatedEnd
///
///
///
///
internal override bool RemoveFromLocalCache(IEntityWrapper wrappedEntity, bool resetIsLoaded, bool preserveForeignKey)
{
Debug.Assert(wrappedEntity != null, "IEntityWrapper instance is null.");
if (_wrappedRelatedEntities != null && _wrappedRelatedEntities.Remove((TEntity)wrappedEntity.Entity))
{
if (resetIsLoaded)
{
_isLoaded = false;
}
return true;
}
return false;
}
///
/// Remove from the POCO collection
///
///
///
internal override bool RemoveFromObjectCache(IEntityWrapper wrappedEntity)
{
Debug.Assert(wrappedEntity != null, "IEntityWrapper instance is null.");
// For POCO entities - remove the object from the CLR collection
if (this.TargetAccessor.HasProperty) // Null if the navigation does not exist in this direction
{
return this.WrappedOwner.CollectionRemove(this, (TEntity)wrappedEntity.Entity);
}
return false;
}
internal override void RetrieveReferentialConstraintProperties(Dictionary> properties, HashSet