//----------------------------------------------------------------------
//
// 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.Diagnostics;
using System.Data.Metadata.Edm;
namespace System.Data.Objects.DataClasses
{
///
/// Collection of entities modelling a particular EDM construct
/// which can either be all entiteis of a particular type or
/// entities participating in a particular relationship.
///
[Serializable]
public sealed class EntityCollection : RelatedEnd, ICollection, IListSource
where TEntity : class, IEntityWithRelationships
{
// ------
// 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.
private HashSet _relatedEntities;
[NonSerialized]
private CollectionChangeEventHandler _onAssociationChangedforObjectView;
// ------------
// Constructors
// ------------
///
/// Creates an empty EntityCollection.
///
public EntityCollection()
: base()
{
}
internal EntityCollection(IEntityWithRelationships owner, RelationshipNavigation navigation, IRelationshipFixer relationshipFixer)
: base(owner, 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;
}
}
// ---------
// Propertites
// ---------
private HashSet RelatedEntities
{
get {
if (null == _relatedEntities)
{
_relatedEntities = new HashSet();
}
return _relatedEntities;
}
}
// ----------------------
// ICollection Properties
// ----------------------
///
/// Count of entities in the collection.
///
public int Count
{
get
{
// count should not cause allocation
return ((null != _relatedEntities) ? _relatedEntities.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)
{
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()
{
CheckOwnerNull();
EntitySet singleEntitySet = null;
EntityType rootEntityType = 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((IEnumerable)null, mergeOption);
// do not fire the AssociationChanged event here,
// once it is fired in one level deeper, (at Internal void Load(IEnumerable)), you dont 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();
Attach(entities, 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)
{
((IRelatedEnd)this).Attach(entity);
}
///
/// 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(IEnumerable collection, MergeOption mergeOption)
{
// Validate that the Load is possible
ObjectQuery sourceQuery = ValidateLoad(mergeOption, "EntityCollection");
// 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
{
IEnumerable loadSource = collection ?? GetResults(sourceQuery);
Merge(loadSource, mergeOption, true /*setIsLoaded*/);
}
finally
{
_suppressEvents = false;
}
// fire the AssociationChange with Refresh
OnAssociationChanged(CollectionChangeAction.Refresh, null);
}
///
///
///
public void Add(TEntity entity)
{
EntityUtil.CheckArgumentNull(entity, "entity");
if (this.Owner != null)
{
((IRelatedEnd)this).Add((TEntity)entity);
}
else
{
// The EntityCollection is operating in a disconnected state
// This is common in WCF deserialization
DisconnectedAdd(entity);
}
}
///
/// Add the item to the underlying collection
///
///
///
internal override void DisconnectedAdd(IEntityWithRelationships entity)
{
// Validate that the incoming entity is also detached
if (null != entity.RelationshipManager.Context && entity.RelationshipManager.MergeOption != MergeOption.NoTracking)
{
throw EntityUtil.UnableToAddToDisconnectedRelatedEnd();
}
// Add the entity to local collection without doing any fixup
AddEntityToLocallyCachedCollection(entity, /* applyConstraints */ false);
}
///
/// Remove the item from the underlying collection
///
///
///
internal override bool DisconnectedRemove(IEntityWithRelationships entity)
{
// Validate that the incoming entity is also detached
if (null != entity.RelationshipManager.Context && entity.RelationshipManager.MergeOption != MergeOption.NoTracking)
{
throw EntityUtil.UnableToRemoveFromDisconnectedRelatedEnd();
}
// Remove the entity to local collection without doing any fixup
return RemoveEntityFromLocallyCachedCollection(entity, /* resetIsLoaded*/ false);
}
///
/// 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");
return ((IRelatedEnd)this).Remove((TEntity)entity);
}
internal override void Include(bool addRelationshipAsUnchanged, bool doAttach, HashSet promotedEntityKeyRefs)
{
if (null != _relatedEntities && null != this.ObjectContext)
{
foreach (TEntity e in _relatedEntities)
{
IncludeEntity(e, addRelationshipAsUnchanged, doAttach, promotedEntityKeyRefs);
}
}
}
internal override void Exclude(HashSet promotedEntityKeyRefs)
{
if (null != _relatedEntities && null != this.ObjectContext)
{
foreach (TEntity e in _relatedEntities)
{
ExcludeEntity(e, promotedEntityKeyRefs);
}
}
}
internal override void ClearCollectionOrRef(IEntityWithRelationships entity, RelationshipNavigation navigation, bool doCascadeDelete)
{
if (null != _relatedEntities)
{
//copy into list because changing collection member is not allowed during enumeration.
// If possible avoid copying into list.
List tempCopy = new List(_relatedEntities);
foreach (TEntity e 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 ((entity == e) && (navigation.Equals(RelationshipNavigation)))
{
Remove(e, /*fixup*/false, /*deleteEntity*/false, /*deleteOwner*/false, /*applyReferentialConstraints*/false);
}
else
{
Remove(e, /*fixup*/true, doCascadeDelete, /*deleteOwner*/false, /*applyReferentialConstraints*/false);
}
}
Debug.Assert(_relatedEntities.Count == 0, "After removing all related entities local collection count should be zero");
}
}
///
///
///
///
///
/// 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.InvalidContainedTypeCollection(entity.GetType().FullName, typeof(TEntity).FullName);
}
return true;
}
//applyConstraints flag is only used in EntityReference
internal override void AddEntityToLocallyCachedCollection(IEntityWithRelationships entity, bool applyConstraints)
{
RelatedEntities.Add((TEntity)entity);
}
internal override bool RemoveEntityFromLocallyCachedCollection(IEntityWithRelationships entity, bool resetIsLoaded)
{
if (_relatedEntities != null && _relatedEntities.Remove((TEntity)entity))
{
if (resetIsLoaded)
_isLoaded = false;
return true;
}
return false;
}
internal override void RetrieveReferentialConstraintProperties(Dictionary> properties, HashSet