//--------------------------------------------------------------------- // // 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 visited) { // Since there are no RI Constraints which has a collection as a To/Child role, // this method is no-op. } internal override bool IsEmpty() { return _wrappedRelatedEntities == null || (_wrappedRelatedEntities.Count == 0); } internal override void VerifyMultiplicityConstraintsForAdd(bool applyConstraints) { // no-op } // Update IsLoaded flag if necessary // This method is called when Clear() was called on the other end of relationship (if the other end is EntityCollection) // or when Value property of the other end was set to null (if the other end is EntityReference). // This method is used only when NoTracking option was used. internal override void OnRelatedEndClear() { // If other end of relationship was cleared, it means that this collection is also no longer loaded _isLoaded = false; } internal override bool ContainsEntity(IEntityWrapper wrappedEntity) { Debug.Assert(wrappedEntity != null, "IEntityWrapper instance is null."); // Using operator 'as' instead of () allows calling ContainsEntity // with entity of different type than TEntity. TEntity entity = wrappedEntity.Entity as TEntity; return _wrappedRelatedEntities == null ? false : _wrappedRelatedEntities.ContainsKey(entity); } // ------------------- // ICollection Methods // ------------------- /// /// Get an enumerator for the collection. /// public new IEnumerator GetEnumerator() { DeferredLoad(); return WrappedRelatedEntities.Keys.GetEnumerator(); } IEnumerator System.Collections.IEnumerable.GetEnumerator() { DeferredLoad(); return WrappedRelatedEntities.Keys.GetEnumerator(); } internal override IEnumerable GetInternalEnumerable() { return WrappedRelatedEntities.Keys; } internal override IEnumerable GetWrappedEntities() { return WrappedRelatedEntities.Values; } /// /// Removes all entities from the locally cached collection. Also removes /// relationships related to this entities from the ObjectStateManager. /// public void Clear() { DeferredLoad(); if (WrappedOwner.Entity != null) { bool shouldFireEvent = (CountInternal > 0); if (null != _wrappedRelatedEntities) { List affectedEntities = new List(_wrappedRelatedEntities.Values); try { _suppressEvents = true; foreach (IEntityWrapper wrappedEntity in affectedEntities) { // Remove Entity Remove(wrappedEntity, false); if (UsingNoTracking) { // 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(wrappedEntity); relatedEnd.OnRelatedEndClear(); } } Debug.Assert(_wrappedRelatedEntities.Count == 0); } finally { _suppressEvents = false; } if (UsingNoTracking) { _isLoaded = false; } } if (shouldFireEvent) { OnAssociationChanged(CollectionChangeAction.Refresh, null); } } else { // Disconnected Clear should be dispatched to the internal collection if (_wrappedRelatedEntities != null) { _wrappedRelatedEntities.Clear(); } } } /// /// Determine if the collection contains a specific object by reference. /// /// true if the collection contains the object by reference; /// otherwise, false public bool Contains(TEntity entity) { DeferredLoad(); return _wrappedRelatedEntities == null ? false : _wrappedRelatedEntities.ContainsKey(entity); } /// /// Copies the contents of the collection to an array, /// starting at a particular array index. /// public void CopyTo(TEntity[] array, int arrayIndex) { DeferredLoad(); WrappedRelatedEntities.Keys.CopyTo(array, arrayIndex); } internal override void BulkDeleteAll(List list) { if (list.Count > 0) { _suppressEvents = true; try { foreach (object entity in list) { // Remove Entity RemoveInternal(entity as TEntity); } } finally { _suppressEvents = false; } OnAssociationChanged(CollectionChangeAction.Refresh, null); } } internal override bool CheckIfNavigationPropertyContainsEntity(IEntityWrapper wrapper) { Debug.Assert(this.RelationshipNavigation != null, "null RelationshipNavigation"); // If the navigation property doesn't exist (e.g. unidirectional prop), then it can't contain the entity. if (!TargetAccessor.HasProperty) { return false; } object value = this.WrappedOwner.GetNavigationPropertyValue(this); if (value != null) { if (!(value is IEnumerable)) { throw new EntityException(System.Data.Entity.Strings.ObjectStateEntry_UnableToEnumerateCollection( this.TargetAccessor.PropertyName, this.WrappedOwner.Entity.GetType().FullName)); } // foreach (object o in (value as IEnumerable)) { if (Object.Equals(o, wrapper.Entity)) { return true; } } } return false; } internal override void VerifyNavigationPropertyForAdd(IEntityWrapper wrapper) { // no-op } // This method is required to maintain compatibility with the v1 binary serialization format. // In particular, it takes the dictionary of wrapped entities and creates a hash set of // raw entities that will be serialized. // Note that this is only expected to work for non-POCO entities, since serialization of POCO // entities will not result in serialization of the RelationshipManager or its related objects. [OnSerializing()] [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] public void OnSerializing(StreamingContext context) { if (!(WrappedOwner.Entity is IEntityWithRelationships)) { throw new InvalidOperationException(System.Data.Entity.Strings.RelatedEnd_CannotSerialize("EntityCollection")); } _relatedEntities = _wrappedRelatedEntities == null ? null : new HashSet(_wrappedRelatedEntities.Keys); } // This method is required to maintain compatibility with the v1 binary serialization format. // In particular, it takes the _relatedEntities HashSet and recreates the dictionary of wrapped // entities from it. This is because the dictionary is not serialized. // Note that this is only expected to work for non-POCO entities, since serialization of POCO // entities will not result in serialization of the RelationshipManager or its related objects. [OnDeserialized()] [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] public void OnCollectionDeserialized(StreamingContext context) { if (_relatedEntities != null) { // We need to call this here so that the hash set will be fully constructed // ready for access. Normally, this would happen later in the process. _relatedEntities.OnDeserialization(null); _wrappedRelatedEntities = new Dictionary(); foreach (TEntity entity in _relatedEntities) { _wrappedRelatedEntities.Add(entity, EntityWrapperFactory.WrapEntityUsingContext(entity, ObjectContext)); } } } // Identical code is in EntityReference, but this can't be moved to the base class because it relies on the // knowledge of the generic type, and the base class isn't generic public ObjectQuery CreateSourceQuery() { CheckOwnerNull(); bool hasResults; return CreateSourceQuery(DefaultMergeOption, out hasResults); } internal override IEnumerable CreateSourceQueryInternal() { return CreateSourceQuery(); } //End identical code #region Add internal override void AddToLocalCache(IEntityWrapper wrappedEntity, bool applyConstraints) { Debug.Assert(wrappedEntity != null, "IEntityWrapper instance is null."); WrappedRelatedEntities[(TEntity)wrappedEntity.Entity] = wrappedEntity; } internal override void AddToObjectCache(IEntityWrapper wrappedEntity) { Debug.Assert(wrappedEntity != null, "IEntityWrapper instance is null."); // For POCO entities - add the object to the CLR collection if (this.TargetAccessor.HasProperty) // Null if the navigation does not exist in this direction { this.WrappedOwner.CollectionAdd(this, wrappedEntity.Entity); } } #endregion } }