1678 lines
72 KiB
C#
1678 lines
72 KiB
C#
//------------------------------------------------------------------------------
|
|
// <copyright file="Shaper.cs" company="Microsoft">
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// </copyright>
|
|
// <owner current="true" primary="true">[....]</owner>
|
|
// <owner current="true" primary="false">[....]</owner>
|
|
//------------------------------------------------------------------------------
|
|
|
|
namespace System.Data.Common.Internal.Materialization
|
|
{
|
|
using System.Collections.Generic;
|
|
using System.Data.Common.Utils;
|
|
using System.Data.Metadata.Edm;
|
|
using System.Data.Objects;
|
|
using System.Data.Objects.DataClasses;
|
|
using System.Data.Objects.Internal;
|
|
using System.Data.Spatial;
|
|
using System.Diagnostics;
|
|
using System.Reflection;
|
|
|
|
/// <summary>
|
|
/// Shapes store reader values into EntityClient/ObjectQuery results. Also maintains
|
|
/// state used by materializer delegates.
|
|
/// </summary>
|
|
internal abstract class Shaper
|
|
{
|
|
#region constructor
|
|
|
|
internal Shaper(DbDataReader reader, ObjectContext context, MetadataWorkspace workspace, MergeOption mergeOption, int stateCount)
|
|
{
|
|
Debug.Assert(context == null || workspace == context.MetadataWorkspace, "workspace must match context's workspace");
|
|
|
|
this.Reader = reader;
|
|
this.MergeOption = mergeOption;
|
|
this.State = new object[stateCount];
|
|
this.Context = context;
|
|
this.Workspace = workspace;
|
|
this.AssociationSpaceMap = new Dictionary<AssociationType, AssociationType>();
|
|
this.spatialReader = new Singleton<DbSpatialDataReader>(CreateSpatialDataReader);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region OnMaterialized storage
|
|
|
|
/// <summary>
|
|
/// Keeps track of the entities that have been materialized so that we can fire an OnMaterialized
|
|
/// for them before returning control to the caller.
|
|
/// </summary>
|
|
private IList<IEntityWrapper> _materializedEntities;
|
|
|
|
#endregion
|
|
|
|
#region runtime callable/accessible code
|
|
|
|
// Code in this section is called from the delegates produced by the Translator. It
|
|
// may not show up if you search using Find All References...use Find in Files instead.
|
|
//
|
|
// Many items on this class are public, simply to make the job of producing the
|
|
// expressions that use them simpler. If you have a hankering to make them private,
|
|
// you will need to modify the code in the Translator that does the GetMethod/GetField
|
|
// to use BindingFlags.NonPublic | BindingFlags.Instance as well.
|
|
//
|
|
// Debug.Asserts that fire from the code in this region will probably create a
|
|
// SecurityException in the Coordinator's Read method since those are restricted when
|
|
// running the Shaper.
|
|
|
|
/// <summary>
|
|
/// The store data reader we're pulling data from
|
|
/// </summary>
|
|
public readonly DbDataReader Reader;
|
|
|
|
/// <summary>
|
|
/// The state slots we use in the coordinator expression.
|
|
/// </summary>
|
|
public readonly object[] State;
|
|
|
|
/// <summary>
|
|
/// The context the shaper is performing for.
|
|
/// </summary>
|
|
public readonly ObjectContext Context;
|
|
|
|
/// <summary>
|
|
/// The workspace we are performing for; yes we could get it from the context, but
|
|
/// it's much easier to just have it handy.
|
|
/// </summary>
|
|
public readonly MetadataWorkspace Workspace;
|
|
|
|
/// <summary>
|
|
/// The merge option this shaper is performing under/for.
|
|
/// </summary>
|
|
public readonly MergeOption MergeOption;
|
|
|
|
/// <summary>
|
|
/// A mapping of CSpace AssociationTypes to OSpace AssociationTypes
|
|
/// Used for faster lookup/retrieval of AssociationTypes during materialization
|
|
/// </summary>
|
|
private readonly Dictionary<AssociationType, AssociationType> AssociationSpaceMap;
|
|
|
|
/// <summary>
|
|
/// Caches Tuples of EntitySet, AssociationType, and source member name for which RelatedEnds exist.
|
|
/// </summary>
|
|
private HashSet<Tuple<string, string, string>> _relatedEndCache;
|
|
|
|
/// <summary>
|
|
/// Utility method used to evaluate a multi-discriminator column map. Takes
|
|
/// discriminator values and determines the appropriate entity type, then looks up
|
|
/// the appropriate handler and invokes it.
|
|
/// </summary>
|
|
public TElement Discriminate<TElement>(object[] discriminatorValues, Func<object[], EntityType> discriminate, KeyValuePair<EntityType, Func<Shaper, TElement>>[] elementDelegates)
|
|
{
|
|
EntityType entityType = discriminate(discriminatorValues);
|
|
Func<Shaper, TElement> elementDelegate = null;
|
|
foreach (KeyValuePair<EntityType, Func<Shaper, TElement>> typeDelegatePair in elementDelegates)
|
|
{
|
|
if (typeDelegatePair.Key == entityType)
|
|
{
|
|
elementDelegate = typeDelegatePair.Value;
|
|
}
|
|
}
|
|
return elementDelegate(this);
|
|
}
|
|
|
|
public IEntityWrapper HandleEntityNoTracking<TEntity>(IEntityWrapper wrappedEntity)
|
|
{
|
|
Debug.Assert(null != wrappedEntity, "wrapped entity is null");
|
|
RegisterMaterializedEntityForEvent(wrappedEntity);
|
|
return wrappedEntity;
|
|
}
|
|
|
|
/// <summary>
|
|
/// REQUIRES:: entity is not null and MergeOption is OverwriteChanges or PreserveChanges
|
|
/// Handles state management for an entity returned by a query. Where an existing entry
|
|
/// exists, updates that entry and returns the existing entity. Otherwise, the entity
|
|
/// passed in is returned.
|
|
/// </summary>
|
|
public IEntityWrapper HandleEntity<TEntity>(IEntityWrapper wrappedEntity, EntityKey entityKey, EntitySet entitySet)
|
|
{
|
|
Debug.Assert(MergeOption.NoTracking != this.MergeOption, "no need to HandleEntity if there's no tracking");
|
|
Debug.Assert(MergeOption.AppendOnly != this.MergeOption, "use HandleEntityAppendOnly instead...");
|
|
Debug.Assert(null != wrappedEntity, "wrapped entity is null");
|
|
Debug.Assert(null != wrappedEntity.Entity, "if HandleEntity is called, there must be an entity");
|
|
|
|
IEntityWrapper result = wrappedEntity;
|
|
|
|
// no entity set, so no tracking is required for this entity
|
|
if (null != (object)entityKey)
|
|
{
|
|
Debug.Assert(null != entitySet, "if there is an entity key, there must also be an entity set");
|
|
|
|
// check for an existing entity with the same key
|
|
EntityEntry existingEntry = this.Context.ObjectStateManager.FindEntityEntry(entityKey);
|
|
if (null != existingEntry && !existingEntry.IsKeyEntry)
|
|
{
|
|
Debug.Assert(existingEntry.EntityKey.Equals(entityKey), "Found ObjectStateEntry with wrong EntityKey");
|
|
UpdateEntry<TEntity>(wrappedEntity, existingEntry);
|
|
result = existingEntry.WrappedEntity;
|
|
}
|
|
else
|
|
{
|
|
RegisterMaterializedEntityForEvent(result);
|
|
if (null == existingEntry)
|
|
{
|
|
Context.ObjectStateManager.AddEntry(wrappedEntity, entityKey, entitySet, "HandleEntity", false);
|
|
}
|
|
else
|
|
{
|
|
Context.ObjectStateManager.PromoteKeyEntry(existingEntry, wrappedEntity, (IExtendedDataRecord)null, false, /*setIsLoaded*/ true, /*keyEntryInitialized*/ false, "HandleEntity");
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// REQUIRES:: entity exists; MergeOption is AppendOnly
|
|
/// Handles state management for an entity with the given key. When the entity already exists
|
|
/// in the state manager, it is returned directly. Otherwise, the entityDelegate is invoked and
|
|
/// the resulting entity is returned.
|
|
/// </summary>
|
|
public IEntityWrapper HandleEntityAppendOnly<TEntity>(Func<Shaper, IEntityWrapper> constructEntityDelegate, EntityKey entityKey, EntitySet entitySet)
|
|
{
|
|
Debug.Assert(this.MergeOption == MergeOption.AppendOnly, "only use HandleEntityAppendOnly when MergeOption is AppendOnly");
|
|
Debug.Assert(null != constructEntityDelegate, "must provide delegate to construct the entity");
|
|
|
|
IEntityWrapper result;
|
|
|
|
if (null == (object)entityKey)
|
|
{
|
|
// no entity set, so no tracking is required for this entity, just
|
|
// call the delegate to "materialize" it.
|
|
result = constructEntityDelegate(this);
|
|
RegisterMaterializedEntityForEvent(result);
|
|
}
|
|
else
|
|
{
|
|
Debug.Assert(null != entitySet, "if there is an entity key, there must also be an entity set");
|
|
|
|
// check for an existing entity with the same key
|
|
EntityEntry existingEntry = this.Context.ObjectStateManager.FindEntityEntry(entityKey);
|
|
if (null != existingEntry && !existingEntry.IsKeyEntry)
|
|
{
|
|
Debug.Assert(existingEntry.EntityKey.Equals(entityKey), "Found ObjectStateEntry with wrong EntityKey");
|
|
if (typeof(TEntity) != existingEntry.WrappedEntity.IdentityType)
|
|
{
|
|
throw EntityUtil.RecyclingEntity(existingEntry.EntityKey, typeof(TEntity), existingEntry.WrappedEntity.IdentityType);
|
|
}
|
|
|
|
if (EntityState.Added == existingEntry.State)
|
|
{
|
|
throw EntityUtil.AddedEntityAlreadyExists(existingEntry.EntityKey);
|
|
}
|
|
result = existingEntry.WrappedEntity;
|
|
}
|
|
else
|
|
{
|
|
// We don't already have the entity, so construct it
|
|
result = constructEntityDelegate(this);
|
|
RegisterMaterializedEntityForEvent(result);
|
|
if (null == existingEntry)
|
|
{
|
|
Context.ObjectStateManager.AddEntry(result, entityKey, entitySet, "HandleEntity", false);
|
|
}
|
|
else
|
|
{
|
|
Context.ObjectStateManager.PromoteKeyEntry(existingEntry, result, (IExtendedDataRecord)null, false, /*setIsLoaded*/ true, /*keyEntryInitialized*/ false, "HandleEntity");
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Call to ensure a collection of full-spanned elements are added
|
|
/// into the state manager properly. We registers an action to be called
|
|
/// when the collection is closed that pulls the collection of full spanned
|
|
/// objects into the state manager.
|
|
/// </summary>
|
|
public IEntityWrapper HandleFullSpanCollection<T_SourceEntity, T_TargetEntity>(IEntityWrapper wrappedEntity, Coordinator<T_TargetEntity> coordinator, AssociationEndMember targetMember)
|
|
{
|
|
Debug.Assert(null != wrappedEntity, "wrapped entity is null");
|
|
if (null != wrappedEntity.Entity)
|
|
{
|
|
coordinator.RegisterCloseHandler((state, spannedEntities) => FullSpanAction(wrappedEntity, spannedEntities, targetMember));
|
|
}
|
|
return wrappedEntity;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Call to ensure a single full-spanned element is added into
|
|
/// the state manager properly.
|
|
/// </summary>
|
|
public IEntityWrapper HandleFullSpanElement<T_SourceEntity, T_TargetEntity>(IEntityWrapper wrappedSource, IEntityWrapper wrappedSpannedEntity, AssociationEndMember targetMember)
|
|
{
|
|
Debug.Assert(null != wrappedSource, "wrapped entity is null");
|
|
if (wrappedSource.Entity == null)
|
|
{
|
|
return wrappedSource;
|
|
}
|
|
List<IEntityWrapper> spannedEntities = null;
|
|
if (wrappedSpannedEntity.Entity != null)
|
|
{
|
|
// There was a single entity in the column
|
|
// Create a list so we can perform the same logic as a collection of entities
|
|
spannedEntities = new List<IEntityWrapper>(1);
|
|
spannedEntities.Add(wrappedSpannedEntity);
|
|
}
|
|
else
|
|
{
|
|
EntityKey sourceKey = wrappedSource.EntityKey;
|
|
CheckClearedEntryOnSpan(null, wrappedSource, sourceKey, targetMember);
|
|
}
|
|
FullSpanAction(wrappedSource, spannedEntities, targetMember);
|
|
return wrappedSource;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Call to ensure a target entities key is added into the state manager
|
|
/// properly
|
|
/// </summary>
|
|
public IEntityWrapper HandleRelationshipSpan<T_SourceEntity>(IEntityWrapper wrappedEntity, EntityKey targetKey, AssociationEndMember targetMember)
|
|
{
|
|
if (null == wrappedEntity.Entity)
|
|
{
|
|
return wrappedEntity;
|
|
}
|
|
Debug.Assert(targetMember != null);
|
|
Debug.Assert(targetMember.RelationshipMultiplicity == RelationshipMultiplicity.One || targetMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne);
|
|
|
|
EntityKey sourceKey = wrappedEntity.EntityKey;
|
|
AssociationEndMember sourceMember = MetadataHelper.GetOtherAssociationEnd(targetMember);
|
|
CheckClearedEntryOnSpan(targetKey, wrappedEntity, sourceKey, targetMember);
|
|
|
|
if (null != (object)targetKey)
|
|
{
|
|
EntitySet targetEntitySet;
|
|
|
|
EntityContainer entityContainer = this.Context.MetadataWorkspace.GetEntityContainer(
|
|
targetKey.EntityContainerName, DataSpace.CSpace);
|
|
|
|
// find the correct AssociationSet
|
|
AssociationSet associationSet = MetadataHelper.GetAssociationsForEntitySetAndAssociationType(entityContainer,
|
|
targetKey.EntitySetName, (AssociationType)(targetMember.DeclaringType), targetMember.Name, out targetEntitySet);
|
|
Debug.Assert(associationSet != null, "associationSet should not be null");
|
|
|
|
ObjectStateManager manager = Context.ObjectStateManager;
|
|
EntityState newEntryState;
|
|
// If there is an existing relationship entry, update it based on its current state and the MergeOption, otherwise add a new one
|
|
if (!ObjectStateManager.TryUpdateExistingRelationships(this.Context, this.MergeOption, associationSet, sourceMember, sourceKey, wrappedEntity, targetMember, targetKey, /*setIsLoaded*/ true, out newEntryState))
|
|
{
|
|
// Try to find a state entry for the target key
|
|
EntityEntry targetEntry = null;
|
|
if (!manager.TryGetEntityEntry(targetKey, out targetEntry))
|
|
{
|
|
// no entry exists for the target key
|
|
// create a key entry for the target
|
|
targetEntry = manager.AddKeyEntry(targetKey, targetEntitySet);
|
|
}
|
|
|
|
// SQLBU 557105. For 1-1 relationships we have to take care of the relationships of targetEntity
|
|
bool needNewRelationship = true;
|
|
switch (sourceMember.RelationshipMultiplicity)
|
|
{
|
|
case RelationshipMultiplicity.ZeroOrOne:
|
|
case RelationshipMultiplicity.One:
|
|
// devnote: targetEntry can be a key entry (targetEntry.Entity == null),
|
|
// but it that case this parameter won't be used in TryUpdateExistingRelationships
|
|
needNewRelationship = !ObjectStateManager.TryUpdateExistingRelationships(this.Context,
|
|
this.MergeOption,
|
|
associationSet,
|
|
targetMember,
|
|
targetKey,
|
|
targetEntry.WrappedEntity,
|
|
sourceMember,
|
|
sourceKey,
|
|
/*setIsLoaded*/ true,
|
|
out newEntryState);
|
|
|
|
// It is possible that as part of removing existing relationships, the key entry was deleted
|
|
// If that is the case, recreate the key entry
|
|
if (targetEntry.State == EntityState.Detached)
|
|
{
|
|
targetEntry = manager.AddKeyEntry(targetKey, targetEntitySet);
|
|
}
|
|
break;
|
|
case RelationshipMultiplicity.Many:
|
|
// we always need a new relationship with Many-To-Many, if there was no exact match between these two entities, so do nothing
|
|
break;
|
|
default:
|
|
Debug.Assert(false, "Unexpected sourceMember.RelationshipMultiplicity");
|
|
break;
|
|
}
|
|
|
|
if (needNewRelationship)
|
|
{
|
|
|
|
// If the target entry is a key entry, then we need to add a relation
|
|
// between the source and target entries
|
|
// If we are in a state where we just need to add a new Deleted relation, we
|
|
// only need to do that and not touch the related ends
|
|
// If the target entry is a full entity entry, then we need to add
|
|
// the target entity to the source collection or reference
|
|
if (targetEntry.IsKeyEntry || newEntryState == EntityState.Deleted)
|
|
{
|
|
// Add a relationship between the source entity and the target key entry
|
|
RelationshipWrapper wrapper = new RelationshipWrapper(associationSet, sourceMember.Name, sourceKey, targetMember.Name, targetKey);
|
|
manager.AddNewRelation(wrapper, newEntryState);
|
|
}
|
|
else
|
|
{
|
|
Debug.Assert(!targetEntry.IsRelationship, "how IsRelationship?");
|
|
if (targetEntry.State != EntityState.Deleted)
|
|
{
|
|
// The entry contains an entity, do collection or reference fixup
|
|
// This will also try to create a new relationship entry or will revert the delete on an existing deleted relationship
|
|
ObjectStateManager.AddEntityToCollectionOrReference(
|
|
this.MergeOption, wrappedEntity, sourceMember,
|
|
targetEntry.WrappedEntity,
|
|
targetMember,
|
|
/*setIsLoaded*/ true,
|
|
/*relationshipAlreadyExists*/ false,
|
|
/* inKeyEntryPromotion */ false);
|
|
}
|
|
else
|
|
{
|
|
// if the target entry is deleted, then the materializer needs to create a deleted relationship
|
|
// between the entity and the target entry so that if the entity is deleted, the update
|
|
// pipeline can find the relationship (even though it is deleted)
|
|
RelationshipWrapper wrapper = new RelationshipWrapper(associationSet, sourceMember.Name, sourceKey, targetMember.Name, targetKey);
|
|
manager.AddNewRelation(wrapper, EntityState.Deleted);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
RelatedEnd relatedEnd;
|
|
if(TryGetRelatedEnd(wrappedEntity, (AssociationType)targetMember.DeclaringType, sourceMember.Name, targetMember.Name, out relatedEnd))
|
|
{
|
|
SetIsLoadedForSpan(relatedEnd, false);
|
|
}
|
|
}
|
|
|
|
// else there is nothing else for us to do, the relationship has been handled already
|
|
return wrappedEntity;
|
|
}
|
|
|
|
private bool TryGetRelatedEnd(IEntityWrapper wrappedEntity, AssociationType associationType, string sourceEndName, string targetEndName, out RelatedEnd relatedEnd)
|
|
{
|
|
Debug.Assert(associationType.DataSpace == DataSpace.CSpace);
|
|
|
|
// Get the OSpace AssociationType
|
|
AssociationType oSpaceAssociation;
|
|
if (!AssociationSpaceMap.TryGetValue((AssociationType)associationType, out oSpaceAssociation))
|
|
{
|
|
oSpaceAssociation = this.Workspace.GetItemCollection(DataSpace.OSpace).GetItem<AssociationType>(associationType.FullName);
|
|
AssociationSpaceMap[(AssociationType)associationType] = oSpaceAssociation;
|
|
}
|
|
|
|
AssociationEndMember sourceEnd = null;
|
|
AssociationEndMember targetEnd = null;
|
|
foreach (var end in oSpaceAssociation.AssociationEndMembers)
|
|
{
|
|
if (end.Name == sourceEndName)
|
|
{
|
|
sourceEnd = end;
|
|
}
|
|
else if (end.Name == targetEndName)
|
|
{
|
|
targetEnd = end;
|
|
}
|
|
}
|
|
|
|
if (sourceEnd != null && targetEnd != null)
|
|
{
|
|
bool createRelatedEnd = false;
|
|
if (wrappedEntity.EntityKey == null)
|
|
{
|
|
// Free-floating entity--key is null, so don't have EntitySet for validation, so always create RelatedEnd
|
|
createRelatedEnd = true;
|
|
}
|
|
else
|
|
{
|
|
// It is possible, because of MEST, that we're trying to load a relationship that is valid for this EntityType
|
|
// in metadata, but is not valid in this case because the specific entity is part of an EntitySet that is not
|
|
// mapped in any AssociationSet for this association type.
|
|
// The metadata structure makes checking for this somewhat time consuming because of the loop required.
|
|
// Because the whole reason for this method is perf, we try to reduce the
|
|
// impact of this check by caching positive hits in a HashSet so we don't have to do this for
|
|
// every entity in a query. (We could also cache misses, but since these only happen in MEST, which
|
|
// is not common, we decided not to slow down the normal non-MEST case anymore by doing this.)
|
|
var entitySet = wrappedEntity.EntityKey.GetEntitySet(this.Workspace);
|
|
var relatedEndKey = Tuple.Create<string, string, string>(entitySet.Identity, associationType.Identity, sourceEndName);
|
|
|
|
if (_relatedEndCache == null)
|
|
{
|
|
_relatedEndCache = new HashSet<Tuple<string, string, string>>();
|
|
}
|
|
|
|
if (_relatedEndCache.Contains(relatedEndKey))
|
|
{
|
|
createRelatedEnd = true;
|
|
}
|
|
else
|
|
{
|
|
foreach (var entitySetBase in entitySet.EntityContainer.BaseEntitySets)
|
|
{
|
|
if ((EdmType)entitySetBase.ElementType == associationType)
|
|
{
|
|
if (((AssociationSet)entitySetBase).AssociationSetEnds[sourceEndName].EntitySet == entitySet)
|
|
{
|
|
createRelatedEnd = true;
|
|
_relatedEndCache.Add(relatedEndKey);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (createRelatedEnd)
|
|
{
|
|
relatedEnd = LightweightCodeGenerator.GetRelatedEnd(wrappedEntity.RelationshipManager, sourceEnd, targetEnd, null);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
relatedEnd = null;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the IsLoaded flag to "true"
|
|
/// There are also rules for when this can be set based on MergeOption and the current value(s) in the related end.
|
|
/// </summary>
|
|
private void SetIsLoadedForSpan(RelatedEnd relatedEnd, bool forceToTrue)
|
|
{
|
|
Debug.Assert(relatedEnd != null, "RelatedEnd should not be null");
|
|
|
|
// We can now say this related end is "Loaded"
|
|
// The cases where we should set this to true are:
|
|
// AppendOnly: the related end is empty and does not point to a stub
|
|
// PreserveChanges: the related end is empty and does not point to a stub (otherwise, an Added item exists and IsLoaded should not change)
|
|
// OverwriteChanges: always
|
|
// NoTracking: always
|
|
if (!forceToTrue)
|
|
{
|
|
// Detect the empty value state of the relatedEnd
|
|
forceToTrue = relatedEnd.IsEmpty();
|
|
EntityReference reference = relatedEnd as EntityReference;
|
|
if (reference != null)
|
|
{
|
|
forceToTrue &= reference.EntityKey == null;
|
|
}
|
|
}
|
|
if (forceToTrue || this.MergeOption == MergeOption.OverwriteChanges)
|
|
{
|
|
relatedEnd.SetIsLoaded(true);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// REQUIRES:: entity is not null and MergeOption is OverwriteChanges or PreserveChanges
|
|
/// Calls through to HandleEntity after retrieving the EntityKey from the given entity.
|
|
/// Still need this so that the correct key will be used for iPOCOs that implement IEntityWithKey
|
|
/// in a non-default manner.
|
|
/// </summary>
|
|
public IEntityWrapper HandleIEntityWithKey<TEntity>(IEntityWrapper wrappedEntity, EntitySet entitySet)
|
|
{
|
|
Debug.Assert(null != wrappedEntity, "wrapped entity is null");
|
|
return HandleEntity<TEntity>(wrappedEntity, wrappedEntity.EntityKey, entitySet);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calls through to the specified RecordState to set the value for the specified column ordinal.
|
|
/// </summary>
|
|
public bool SetColumnValue(int recordStateSlotNumber, int ordinal, object value)
|
|
{
|
|
RecordState recordState = (RecordState)this.State[recordStateSlotNumber];
|
|
recordState.SetColumnValue(ordinal, value);
|
|
return true; // TRICKY: return true so we can use BitwiseOr expressions to string these guys together.
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calls through to the specified RecordState to set the value for the EntityRecordInfo.
|
|
/// </summary>
|
|
public bool SetEntityRecordInfo(int recordStateSlotNumber, EntityKey entityKey, EntitySet entitySet)
|
|
{
|
|
RecordState recordState = (RecordState)this.State[recordStateSlotNumber];
|
|
recordState.SetEntityRecordInfo(entityKey, entitySet);
|
|
return true; // TRICKY: return true so we can use BitwiseOr expressions to string these guys together.
|
|
}
|
|
|
|
/// <summary>
|
|
/// REQUIRES:: should be called only by delegate allocating this state.
|
|
/// Utility method assigning a value to a state slot. Returns an arbitrary value
|
|
/// allowing the method call to be composed in a ShapeEmitter Expression delegate.
|
|
/// </summary>
|
|
public bool SetState<T>(int ordinal, T value)
|
|
{
|
|
this.State[ordinal] = value;
|
|
return true; // TRICKY: return true so we can use BitwiseOr expressions to string these guys together.
|
|
}
|
|
|
|
/// <summary>
|
|
/// REQUIRES:: should be called only by delegate allocating this state.
|
|
/// Utility method assigning a value to a state slot and return the value, allowing
|
|
/// the value to be accessed/set in a ShapeEmitter Expression delegate and later
|
|
/// retrieved.
|
|
/// </summary>
|
|
public T SetStatePassthrough<T>(int ordinal, T value)
|
|
{
|
|
this.State[ordinal] = value;
|
|
return value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used to retrieve a property value with exception handling. Normally compiled
|
|
/// delegates directly call typed methods on the DbDataReader (e.g. GetInt32)
|
|
/// but when an exception occurs we retry using this method to potentially get
|
|
/// a more useful error message to the user.
|
|
/// </summary>
|
|
public TProperty GetPropertyValueWithErrorHandling<TProperty>(int ordinal, string propertyName, string typeName)
|
|
{
|
|
TProperty result = new PropertyErrorHandlingValueReader<TProperty>(propertyName, typeName).GetValue(this.Reader, ordinal);
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used to retrieve a column value with exception handling. Normally compiled
|
|
/// delegates directly call typed methods on the DbDataReader (e.g. GetInt32)
|
|
/// but when an exception occurs we retry using this method to potentially get
|
|
/// a more useful error message to the user.
|
|
/// </summary>
|
|
public TColumn GetColumnValueWithErrorHandling<TColumn>(int ordinal)
|
|
{
|
|
TColumn result = new ColumnErrorHandlingValueReader<TColumn>().GetValue(this.Reader, ordinal);
|
|
return result;
|
|
}
|
|
|
|
private DbSpatialDataReader CreateSpatialDataReader()
|
|
{
|
|
return SpatialHelpers.CreateSpatialDataReader(this.Workspace, this.Reader);
|
|
}
|
|
private readonly Singleton<DbSpatialDataReader> spatialReader;
|
|
|
|
public DbGeography GetGeographyColumnValue(int ordinal)
|
|
{
|
|
return this.spatialReader.Value.GetGeography(ordinal);
|
|
}
|
|
|
|
public DbGeometry GetGeometryColumnValue(int ordinal)
|
|
{
|
|
return this.spatialReader.Value.GetGeometry(ordinal);
|
|
}
|
|
|
|
public TColumn GetSpatialColumnValueWithErrorHandling<TColumn>(int ordinal, PrimitiveTypeKind spatialTypeKind)
|
|
{
|
|
Debug.Assert(spatialTypeKind == PrimitiveTypeKind.Geography || spatialTypeKind == PrimitiveTypeKind.Geometry, "Spatial primitive type kind is not geography or geometry?");
|
|
|
|
TColumn result;
|
|
if (spatialTypeKind == PrimitiveTypeKind.Geography)
|
|
{
|
|
result = new ColumnErrorHandlingValueReader<TColumn>(
|
|
(reader, column) => (TColumn)(object)this.spatialReader.Value.GetGeography(column),
|
|
(reader, column) => this.spatialReader.Value.GetGeography(column)
|
|
).GetValue(this.Reader, ordinal);
|
|
}
|
|
else
|
|
{
|
|
result = new ColumnErrorHandlingValueReader<TColumn>(
|
|
(reader, column) => (TColumn)(object)this.spatialReader.Value.GetGeometry(column),
|
|
(reader, column) => this.spatialReader.Value.GetGeometry(column)
|
|
).GetValue(this.Reader, ordinal);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public TProperty GetSpatialPropertyValueWithErrorHandling<TProperty>(int ordinal, string propertyName, string typeName, PrimitiveTypeKind spatialTypeKind)
|
|
{
|
|
TProperty result;
|
|
if (Helper.IsGeographicTypeKind(spatialTypeKind))
|
|
{
|
|
result = new PropertyErrorHandlingValueReader<TProperty>(propertyName, typeName,
|
|
(reader, column) => (TProperty)(object)this.spatialReader.Value.GetGeography(column),
|
|
(reader, column) => this.spatialReader.Value.GetGeography(column)
|
|
).GetValue(this.Reader, ordinal);
|
|
}
|
|
else
|
|
{
|
|
Debug.Assert(Helper.IsGeometricTypeKind(spatialTypeKind));
|
|
result = new PropertyErrorHandlingValueReader<TProperty>(propertyName, typeName,
|
|
(reader, column) => (TProperty)(object)this.spatialReader.Value.GetGeometry(column),
|
|
(reader, column) => this.spatialReader.Value.GetGeometry(column)
|
|
).GetValue(this.Reader, ordinal);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region helper methods (used by runtime callable code)
|
|
|
|
private void CheckClearedEntryOnSpan(object targetValue, IEntityWrapper wrappedSource, EntityKey sourceKey, AssociationEndMember targetMember)
|
|
{
|
|
// If a relationship does not exist on the server but does exist on the client,
|
|
// we may need to remove it, depending on the current state and the MergeOption
|
|
if ((null != (object)sourceKey) && (null == targetValue) &&
|
|
(this.MergeOption == MergeOption.PreserveChanges ||
|
|
this.MergeOption == MergeOption.OverwriteChanges))
|
|
{
|
|
// When the spanned value is null, it may be because the spanned association applies to a
|
|
// subtype of the entity's type, and the entity is not actually an instance of that type.
|
|
AssociationEndMember sourceEnd = MetadataHelper.GetOtherAssociationEnd(targetMember);
|
|
EdmType expectedSourceType = ((RefType)sourceEnd.TypeUsage.EdmType).ElementType;
|
|
TypeUsage entityTypeUsage;
|
|
if (!this.Context.Perspective.TryGetType(wrappedSource.IdentityType, out entityTypeUsage) ||
|
|
entityTypeUsage.EdmType.EdmEquals(expectedSourceType) ||
|
|
TypeSemantics.IsSubTypeOf(entityTypeUsage.EdmType, expectedSourceType))
|
|
{
|
|
// Otherwise, the source entity is the correct type (exactly or a subtype) for the source
|
|
// end of the spanned association, so validate that the relationhip that was spanned is
|
|
// part of the Container owning the EntitySet of the root entity.
|
|
// This can be done by comparing the EntitySet of the row's entity to the relationships
|
|
// in the Container and their AssociationSetEnd's type
|
|
CheckClearedEntryOnSpan(sourceKey, wrappedSource, targetMember);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CheckClearedEntryOnSpan(EntityKey sourceKey, IEntityWrapper wrappedSource, AssociationEndMember targetMember)
|
|
{
|
|
Debug.Assert(null != (object)sourceKey);
|
|
Debug.Assert(wrappedSource != null);
|
|
Debug.Assert(wrappedSource.Entity != null);
|
|
Debug.Assert(targetMember != null);
|
|
Debug.Assert(this.Context != null);
|
|
|
|
AssociationEndMember sourceMember = MetadataHelper.GetOtherAssociationEnd(targetMember);
|
|
|
|
EntityContainer entityContainer = this.Context.MetadataWorkspace.GetEntityContainer(sourceKey.EntityContainerName,
|
|
DataSpace.CSpace);
|
|
EntitySet sourceEntitySet;
|
|
AssociationSet associationSet = MetadataHelper.GetAssociationsForEntitySetAndAssociationType(entityContainer, sourceKey.EntitySetName,
|
|
(AssociationType)sourceMember.DeclaringType, sourceMember.Name, out sourceEntitySet);
|
|
|
|
if (associationSet != null)
|
|
{
|
|
Debug.Assert(associationSet.AssociationSetEnds[sourceMember.Name].EntitySet == sourceEntitySet);
|
|
ObjectStateManager.RemoveRelationships(Context, MergeOption, associationSet, sourceKey, sourceMember);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wire's one or more full-spanned entities into the state manager; used by
|
|
/// both full-spanned collections and full-spanned entities.
|
|
/// </summary>
|
|
private void FullSpanAction<T_TargetEntity>(IEntityWrapper wrappedSource, IList<T_TargetEntity> spannedEntities, AssociationEndMember targetMember)
|
|
{
|
|
Debug.Assert(null != wrappedSource, "wrapped entity is null");
|
|
|
|
if (wrappedSource.Entity != null)
|
|
{
|
|
EntityKey sourceKey = wrappedSource.EntityKey;
|
|
AssociationEndMember sourceMember = MetadataHelper.GetOtherAssociationEnd(targetMember);
|
|
|
|
RelatedEnd relatedEnd;
|
|
if (TryGetRelatedEnd(wrappedSource, (AssociationType)targetMember.DeclaringType, sourceMember.Name, targetMember.Name, out relatedEnd))
|
|
{
|
|
// Add members of the list to the source entity (item in column 0)
|
|
int count = ObjectStateManager.UpdateRelationships(this.Context, this.MergeOption, (AssociationSet)relatedEnd.RelationshipSet, sourceMember, sourceKey, wrappedSource, targetMember, (List<T_TargetEntity>)spannedEntities, true);
|
|
|
|
SetIsLoadedForSpan(relatedEnd, count > 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
#region update existing ObjectStateEntry
|
|
|
|
private void UpdateEntry<TEntity>(IEntityWrapper wrappedEntity, EntityEntry existingEntry)
|
|
{
|
|
Debug.Assert(null != wrappedEntity, "wrapped entity is null");
|
|
Debug.Assert(null != wrappedEntity.Entity, "null entity");
|
|
Debug.Assert(null != existingEntry, "null ObjectStateEntry");
|
|
Debug.Assert(null != existingEntry.Entity, "ObjectStateEntry without Entity");
|
|
|
|
Type clrType = typeof(TEntity);
|
|
if (clrType != existingEntry.WrappedEntity.IdentityType)
|
|
{
|
|
throw EntityUtil.RecyclingEntity(existingEntry.EntityKey, clrType, existingEntry.WrappedEntity.IdentityType);
|
|
}
|
|
|
|
if (EntityState.Added == existingEntry.State)
|
|
{
|
|
throw EntityUtil.AddedEntityAlreadyExists(existingEntry.EntityKey);
|
|
}
|
|
|
|
if (MergeOption.AppendOnly != MergeOption)
|
|
{ // existing entity, update CSpace values in place
|
|
Debug.Assert(EntityState.Added != existingEntry.State, "entry in State=Added");
|
|
Debug.Assert(EntityState.Detached != existingEntry.State, "entry in State=Detached");
|
|
|
|
if (MergeOption.OverwriteChanges == MergeOption)
|
|
{
|
|
if (EntityState.Deleted == existingEntry.State)
|
|
{
|
|
existingEntry.RevertDelete();
|
|
}
|
|
existingEntry.UpdateCurrentValueRecord(wrappedEntity.Entity);
|
|
Context.ObjectStateManager.ForgetEntryWithConceptualNull(existingEntry, resetAllKeys: true);
|
|
existingEntry.AcceptChanges();
|
|
Context.ObjectStateManager.FixupReferencesByForeignKeys(existingEntry, replaceAddedRefs: true);
|
|
}
|
|
else
|
|
{
|
|
Debug.Assert(MergeOption.PreserveChanges == MergeOption, "not MergeOption.PreserveChanges");
|
|
if (EntityState.Unchanged == existingEntry.State)
|
|
{
|
|
// same behavior as MergeOption.OverwriteChanges
|
|
existingEntry.UpdateCurrentValueRecord(wrappedEntity.Entity);
|
|
Context.ObjectStateManager.ForgetEntryWithConceptualNull(existingEntry, resetAllKeys: true);
|
|
existingEntry.AcceptChanges();
|
|
Context.ObjectStateManager.FixupReferencesByForeignKeys(existingEntry, replaceAddedRefs: true);
|
|
}
|
|
else
|
|
{
|
|
if (Context.ContextOptions.UseLegacyPreserveChangesBehavior)
|
|
{
|
|
// Do not mark properties as modified if they differ from the entity.
|
|
existingEntry.UpdateRecordWithoutSetModified(wrappedEntity.Entity, existingEntry.EditableOriginalValues);
|
|
}
|
|
else
|
|
{
|
|
// Mark properties as modified if they differ from the entity
|
|
existingEntry.UpdateRecordWithSetModified(wrappedEntity.Entity, existingEntry.EditableOriginalValues);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#endregion
|
|
|
|
#region nested types
|
|
private abstract class ErrorHandlingValueReader<T>
|
|
{
|
|
private readonly Func<DbDataReader, int, T> getTypedValue;
|
|
private readonly Func<DbDataReader, int, object> getUntypedValue;
|
|
|
|
protected ErrorHandlingValueReader(Func<DbDataReader, int, T> typedValueAccessor, Func<DbDataReader, int, object> untypedValueAccessor)
|
|
{
|
|
this.getTypedValue = typedValueAccessor;
|
|
this.getUntypedValue = untypedValueAccessor;
|
|
}
|
|
|
|
protected ErrorHandlingValueReader()
|
|
: this(GetTypedValueDefault, GetUntypedValueDefault)
|
|
{
|
|
}
|
|
|
|
private static T GetTypedValueDefault(DbDataReader reader, int ordinal)
|
|
{
|
|
var underlyingType = Nullable.GetUnderlyingType(typeof(T));
|
|
// The value read from the reader is of a primitive type. Such a value cannot be cast to a nullable enum type directly
|
|
// but first needs to be cast to the non-nullable enum type. Therefore we will call this method for non-nullable
|
|
// underlying enum type and cast to the target type.
|
|
if (underlyingType != null && underlyingType.IsEnum)
|
|
{
|
|
var type = typeof(ErrorHandlingValueReader<>).MakeGenericType(underlyingType);
|
|
return (T)type.GetMethod(MethodBase.GetCurrentMethod().Name, BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, new object[] { reader, ordinal });
|
|
}
|
|
|
|
// use the specific reader.GetXXX method
|
|
bool isNullable;
|
|
MethodInfo readerMethod = Translator.GetReaderMethod(typeof(T), out isNullable);
|
|
T result = (T)readerMethod.Invoke(reader, new object[] { ordinal });
|
|
return result;
|
|
}
|
|
|
|
private static object GetUntypedValueDefault(DbDataReader reader, int ordinal)
|
|
{
|
|
return reader.GetValue(ordinal);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets value from reader using the same pattern as the materializer delegate. Avoids
|
|
/// the need to compile multiple delegates for error handling. If there is a failure
|
|
/// reading a value
|
|
/// </summary>
|
|
internal T GetValue(DbDataReader reader, int ordinal)
|
|
{
|
|
T result;
|
|
if (reader.IsDBNull(ordinal))
|
|
{
|
|
try
|
|
{
|
|
result = (T)(object)null;
|
|
}
|
|
catch (NullReferenceException)
|
|
{
|
|
// NullReferenceException is thrown when casting null to a value type.
|
|
// We don't use isNullable here because of an issue with GetReaderMethod
|
|
//
|
|
throw CreateNullValueException();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
result = this.getTypedValue(reader, ordinal);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
if (EntityUtil.IsCatchableExceptionType(e))
|
|
{
|
|
// determine if the problem is with the result type
|
|
// (note that if we throw on this call, it's ok
|
|
// for it to percolate up -- we only intercept type
|
|
// and null mismatches)
|
|
object untypedResult = this.getUntypedValue(reader, ordinal);
|
|
Type resultType = null == untypedResult ? null : untypedResult.GetType();
|
|
if (!typeof(T).IsAssignableFrom(resultType))
|
|
{
|
|
throw CreateWrongTypeException(resultType);
|
|
}
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the exception thrown when the reader returns a null value
|
|
/// for a non nullable property/column.
|
|
/// </summary>
|
|
protected abstract Exception CreateNullValueException();
|
|
|
|
/// <summary>
|
|
/// Creates the exception thrown when the reader returns a value with
|
|
/// an incompatible type.
|
|
/// </summary>
|
|
protected abstract Exception CreateWrongTypeException(Type resultType);
|
|
}
|
|
|
|
private class ColumnErrorHandlingValueReader<TColumn> : ErrorHandlingValueReader<TColumn>
|
|
{
|
|
internal ColumnErrorHandlingValueReader()
|
|
{
|
|
}
|
|
|
|
internal ColumnErrorHandlingValueReader(Func<DbDataReader, int, TColumn> typedAccessor, Func<DbDataReader, int, object> untypedAccessor)
|
|
: base(typedAccessor, untypedAccessor)
|
|
{
|
|
}
|
|
|
|
protected override Exception CreateNullValueException()
|
|
{
|
|
return EntityUtil.ValueNullReferenceCast(typeof(TColumn));
|
|
}
|
|
|
|
protected override Exception CreateWrongTypeException(Type resultType)
|
|
{
|
|
return EntityUtil.ValueInvalidCast(resultType, typeof(TColumn));
|
|
}
|
|
}
|
|
|
|
private class PropertyErrorHandlingValueReader<TProperty> : ErrorHandlingValueReader<TProperty>
|
|
{
|
|
private readonly string _propertyName;
|
|
private readonly string _typeName;
|
|
|
|
internal PropertyErrorHandlingValueReader(string propertyName, string typeName)
|
|
: base()
|
|
{
|
|
_propertyName = propertyName;
|
|
_typeName = typeName;
|
|
}
|
|
|
|
internal PropertyErrorHandlingValueReader(string propertyName, string typeName, Func<DbDataReader, int, TProperty> typedAccessor, Func<DbDataReader, int, object> untypedAccessor)
|
|
: base(typedAccessor, untypedAccessor)
|
|
{
|
|
_propertyName = propertyName;
|
|
_typeName = typeName;
|
|
}
|
|
|
|
protected override Exception CreateNullValueException()
|
|
{
|
|
return EntityUtil.Constraint(
|
|
System.Data.Entity.Strings.Materializer_SetInvalidValue(
|
|
(Nullable.GetUnderlyingType(typeof(TProperty)) ?? typeof(TProperty)).Name,
|
|
_typeName, _propertyName, "null"));
|
|
}
|
|
|
|
protected override Exception CreateWrongTypeException(Type resultType)
|
|
{
|
|
return EntityUtil.InvalidOperation(
|
|
System.Data.Entity.Strings.Materializer_SetInvalidValue(
|
|
(Nullable.GetUnderlyingType(typeof(TProperty)) ?? typeof(TProperty)).Name,
|
|
_typeName, _propertyName, resultType.Name));
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region OnMaterialized helpers
|
|
|
|
public void RaiseMaterializedEvents()
|
|
{
|
|
if (_materializedEntities != null)
|
|
{
|
|
foreach (var wrappedEntity in _materializedEntities)
|
|
{
|
|
Context.OnObjectMaterialized(wrappedEntity.Entity);
|
|
}
|
|
_materializedEntities.Clear();
|
|
}
|
|
}
|
|
|
|
public void InitializeForOnMaterialize()
|
|
{
|
|
if (Context.OnMaterializedHasHandlers)
|
|
{
|
|
if (_materializedEntities == null)
|
|
{
|
|
_materializedEntities = new List<IEntityWrapper>();
|
|
}
|
|
}
|
|
else if (_materializedEntities != null)
|
|
{
|
|
_materializedEntities = null;
|
|
}
|
|
}
|
|
|
|
protected void RegisterMaterializedEntityForEvent(IEntityWrapper wrappedEntity)
|
|
{
|
|
if (_materializedEntities != null)
|
|
{
|
|
_materializedEntities.Add(wrappedEntity);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>
|
|
/// Typed Shaper. Includes logic to enumerate results and wraps the _rootCoordinator,
|
|
/// which includes materializer delegates for the root query collection.
|
|
/// </summary>
|
|
internal sealed class Shaper<T> : Shaper
|
|
{
|
|
#region private state
|
|
|
|
/// <summary>
|
|
/// Shapers and Coordinators work together in harmony to materialize the data
|
|
/// from the store; the shaper contains the state, the coordinator contains the
|
|
/// code.
|
|
/// </summary>
|
|
internal readonly Coordinator<T> RootCoordinator;
|
|
|
|
/// <summary>
|
|
/// Which type of query is this, object layer (true) or value layer (false)
|
|
/// </summary>
|
|
private readonly bool IsObjectQuery;
|
|
|
|
/// <summary>
|
|
/// Keeps track of whether we've completed processing or not.
|
|
/// </summary>
|
|
private bool _isActive;
|
|
|
|
/// <summary>
|
|
/// The enumerator we're using to read data; really only populated for value
|
|
/// layer queries.
|
|
/// </summary>
|
|
private IEnumerator<T> _rootEnumerator;
|
|
|
|
/// <summary>
|
|
/// Whether the current value of _rootEnumerator has been returned by a bridge
|
|
/// data reader.
|
|
/// </summary>
|
|
private bool _dataWaiting;
|
|
|
|
/// <summary>
|
|
/// Is the reader owned by the EF or was it supplied by the user?
|
|
/// </summary>
|
|
private bool _readerOwned;
|
|
|
|
#endregion
|
|
|
|
#region constructor
|
|
|
|
internal Shaper(DbDataReader reader, ObjectContext context, MetadataWorkspace workspace, MergeOption mergeOption, int stateCount, CoordinatorFactory<T> rootCoordinatorFactory, Action checkPermissions, bool readerOwned)
|
|
: base(reader, context, workspace, mergeOption, stateCount)
|
|
{
|
|
RootCoordinator = new Coordinator<T>(rootCoordinatorFactory, /*parent*/ null, /*next*/ null);
|
|
if (null != checkPermissions)
|
|
{
|
|
checkPermissions();
|
|
}
|
|
IsObjectQuery = !(typeof(T) == typeof(RecordState));
|
|
_isActive = true;
|
|
RootCoordinator.Initialize(this);
|
|
_readerOwned = readerOwned;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region "public" surface area
|
|
|
|
/// <summary>
|
|
/// Events raised when the shaper has finished enumerating results. Useful for callback
|
|
/// to set parameter values.
|
|
/// </summary>
|
|
internal event EventHandler OnDone;
|
|
|
|
/// <summary>
|
|
/// Used to handle the read-ahead requirements of value-layer queries. This
|
|
/// field indicates the status of the current value of the _rootEnumerator; when
|
|
/// a bridge data reader "accepts responsibility" for the current value, it sets
|
|
/// this to false.
|
|
/// </summary>
|
|
internal bool DataWaiting
|
|
{
|
|
get { return _dataWaiting; }
|
|
set { _dataWaiting = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The enumerator that the value-layer bridge will use to read data; all nested
|
|
/// data readers need to use the same enumerator, so we put it on the Shaper, since
|
|
/// that is something that all the nested data readers (and data records) have access
|
|
/// to -- it prevents us from having to pass two objects around.
|
|
/// </summary>
|
|
internal IEnumerator<T> RootEnumerator
|
|
{
|
|
get
|
|
{
|
|
if (_rootEnumerator == null)
|
|
{
|
|
InitializeRecordStates(RootCoordinator.CoordinatorFactory);
|
|
_rootEnumerator = GetEnumerator();
|
|
}
|
|
return _rootEnumerator;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialize the RecordStateFactory objects in their StateSlots.
|
|
/// </summary>
|
|
private void InitializeRecordStates(CoordinatorFactory coordinatorFactory)
|
|
{
|
|
foreach (RecordStateFactory recordStateFactory in coordinatorFactory.RecordStateFactories)
|
|
{
|
|
State[recordStateFactory.StateSlotNumber] = recordStateFactory.Create(coordinatorFactory);
|
|
}
|
|
|
|
foreach (CoordinatorFactory nestedCoordinatorFactory in coordinatorFactory.NestedCoordinators)
|
|
{
|
|
InitializeRecordStates(nestedCoordinatorFactory);
|
|
}
|
|
}
|
|
|
|
public IEnumerator<T> GetEnumerator()
|
|
{
|
|
// we can use a simple enumerator if there are no nested results, no keys and no "has data"
|
|
// discriminator
|
|
if (RootCoordinator.CoordinatorFactory.IsSimple)
|
|
{
|
|
return new SimpleEnumerator(this);
|
|
}
|
|
else
|
|
{
|
|
RowNestedResultEnumerator rowEnumerator = new Shaper<T>.RowNestedResultEnumerator(this);
|
|
|
|
if (this.IsObjectQuery)
|
|
{
|
|
return new ObjectQueryNestedEnumerator(rowEnumerator);
|
|
}
|
|
else
|
|
{
|
|
return (IEnumerator<T>)(object)(new RecordStateEnumerator(rowEnumerator));
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region enumerator helpers
|
|
|
|
/// <summary>
|
|
/// Called when enumeration of results has completed.
|
|
/// </summary>
|
|
private void Finally()
|
|
{
|
|
if (_isActive)
|
|
{
|
|
_isActive = false;
|
|
|
|
if (_readerOwned)
|
|
{
|
|
// I'd prefer not to special case this, but value-layer behavior is that you
|
|
// must explicitly close the data reader; if we automatically dispose of the
|
|
// reader here, we won't have that behavior.
|
|
if (IsObjectQuery)
|
|
{
|
|
this.Reader.Dispose();
|
|
}
|
|
|
|
// This case includes when the ObjectResult is disposed before it
|
|
// created an ObjectQueryEnumeration; at this time, the connection can be released
|
|
if (this.Context != null)
|
|
{
|
|
this.Context.ReleaseConnection();
|
|
}
|
|
}
|
|
|
|
if (null != this.OnDone)
|
|
{
|
|
this.OnDone(this, new EventArgs());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the next row from the store. If there is a failure, throws an exception message
|
|
/// in some scenarios (note that we respond to failure rather than anticipate failure,
|
|
/// avoiding repeated checks in the inner materialization loop)
|
|
/// </summary>
|
|
private bool StoreRead()
|
|
{
|
|
bool readSucceeded;
|
|
try
|
|
{
|
|
readSucceeded = this.Reader.Read();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
// check if the reader is closed; if so, throw friendlier exception
|
|
if (this.Reader.IsClosed)
|
|
{
|
|
const string operation = "Read";
|
|
throw EntityUtil.DataReaderClosed(operation);
|
|
}
|
|
|
|
// wrap exception if necessary
|
|
if (EntityUtil.IsCatchableEntityExceptionType(e))
|
|
{
|
|
throw EntityUtil.CommandExecution(System.Data.Entity.Strings.EntityClient_StoreReaderFailed, e);
|
|
}
|
|
throw;
|
|
}
|
|
return readSucceeded;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notify ObjectContext that we are about to start materializing an element
|
|
/// </summary>
|
|
private void StartMaterializingElement()
|
|
{
|
|
if (Context != null)
|
|
{
|
|
Context.InMaterialization = true;
|
|
InitializeForOnMaterialize();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notify ObjectContext that we are finished materializing the element
|
|
/// </summary>
|
|
private void StopMaterializingElement()
|
|
{
|
|
if (Context != null)
|
|
{
|
|
Context.InMaterialization = false;
|
|
RaiseMaterializedEvents();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region simple enumerator
|
|
|
|
/// <summary>
|
|
/// Optimized enumerator for queries not including nested results.
|
|
/// </summary>
|
|
private class SimpleEnumerator : IEnumerator<T>
|
|
{
|
|
private readonly Shaper<T> _shaper;
|
|
|
|
internal SimpleEnumerator(Shaper<T> shaper)
|
|
{
|
|
_shaper = shaper;
|
|
}
|
|
|
|
public T Current
|
|
{
|
|
get { return _shaper.RootCoordinator.Current; }
|
|
}
|
|
|
|
object System.Collections.IEnumerator.Current
|
|
{
|
|
get { return _shaper.RootCoordinator.Current; }
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
// Technically, calling GC.SuppressFinalize is not required because the class does not
|
|
// have a finalizer, but it does no harm, protects against the case where a finalizer is added
|
|
// in the future, and prevents an FxCop warning.
|
|
GC.SuppressFinalize(this);
|
|
// For backwards compatibility, we set the current value to the
|
|
// default value, so you can still call Current.
|
|
_shaper.RootCoordinator.SetCurrentToDefault();
|
|
_shaper.Finally();
|
|
}
|
|
|
|
public bool MoveNext()
|
|
{
|
|
if (!_shaper._isActive)
|
|
{
|
|
return false;
|
|
}
|
|
if (_shaper.StoreRead())
|
|
{
|
|
try
|
|
{
|
|
_shaper.StartMaterializingElement();
|
|
_shaper.RootCoordinator.ReadNextElement(_shaper);
|
|
}
|
|
finally
|
|
{
|
|
_shaper.StopMaterializingElement();
|
|
}
|
|
return true;
|
|
}
|
|
this.Dispose();
|
|
return false;
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
throw EntityUtil.NotSupported();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region nested enumerator
|
|
|
|
/// <summary>
|
|
/// Enumerates (for each row in the input) an array of all coordinators producing new elements. The array
|
|
/// contains a position for each 'depth' in the result. A null value in any position indicates that no new
|
|
/// results were produced for the given row at the given depth. It is possible for a row to contain no
|
|
/// results for any row.
|
|
/// </summary>
|
|
private class RowNestedResultEnumerator : IEnumerator<Coordinator[]>
|
|
{
|
|
private readonly Shaper<T> _shaper;
|
|
private readonly Coordinator[] _current;
|
|
|
|
internal RowNestedResultEnumerator(Shaper<T> shaper)
|
|
{
|
|
_shaper = shaper;
|
|
_current = new Coordinator[_shaper.RootCoordinator.MaxDistanceToLeaf() + 1];
|
|
}
|
|
|
|
public Coordinator[] Current
|
|
{
|
|
get { return _current; }
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
// Technically, calling GC.SuppressFinalize is not required because the class does not
|
|
// have a finalizer, but it does no harm, protects against the case where a finalizer is added
|
|
// in the future, and prevents an FxCop warning.
|
|
GC.SuppressFinalize(this);
|
|
_shaper.Finally();
|
|
}
|
|
|
|
object System.Collections.IEnumerator.Current
|
|
{
|
|
get { return _current; }
|
|
}
|
|
|
|
public bool MoveNext()
|
|
{
|
|
Coordinator currentCoordinator = _shaper.RootCoordinator;
|
|
|
|
try
|
|
{
|
|
_shaper.StartMaterializingElement();
|
|
|
|
if (!_shaper.StoreRead())
|
|
{
|
|
// Reset all collections
|
|
this.RootCoordinator.ResetCollection(_shaper);
|
|
return false;
|
|
}
|
|
|
|
int depth = 0;
|
|
bool haveInitializedChildren = false;
|
|
for (; depth < _current.Length; depth++)
|
|
{
|
|
// find a coordinator at this depth that currently has data (if any)
|
|
while (currentCoordinator != null && !currentCoordinator.CoordinatorFactory.HasData(_shaper))
|
|
{
|
|
currentCoordinator = currentCoordinator.Next;
|
|
}
|
|
if (null == currentCoordinator)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// check if this row contains a new element for this coordinator
|
|
if (currentCoordinator.HasNextElement(_shaper))
|
|
{
|
|
// if we have children and haven't initialized them yet, do so now
|
|
if (!haveInitializedChildren && null != currentCoordinator.Child)
|
|
{
|
|
currentCoordinator.Child.ResetCollection(_shaper);
|
|
}
|
|
haveInitializedChildren = true;
|
|
|
|
// read the next element
|
|
currentCoordinator.ReadNextElement(_shaper);
|
|
|
|
// place the coordinator in the result array to indicate there is a new
|
|
// element at this depth
|
|
_current[depth] = currentCoordinator;
|
|
}
|
|
else
|
|
{
|
|
// clear out the coordinator in result array to indicate there is no new
|
|
// element at this depth
|
|
_current[depth] = null;
|
|
}
|
|
|
|
// move to child (in the next iteration we deal with depth + 1
|
|
currentCoordinator = currentCoordinator.Child;
|
|
}
|
|
|
|
// clear out all positions below the depth we reached before we ran out of data
|
|
for (; depth < _current.Length; depth++)
|
|
{
|
|
_current[depth] = null;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_shaper.StopMaterializingElement();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
throw EntityUtil.NotSupported();
|
|
}
|
|
|
|
internal Coordinator<T> RootCoordinator
|
|
{
|
|
get { return _shaper.RootCoordinator; }
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wraps RowNestedResultEnumerator and yields results appropriate to an ObjectQuery instance. In particular,
|
|
/// root level elements (T) are returned only after aggregating all child elements.
|
|
/// </summary>
|
|
private class ObjectQueryNestedEnumerator : IEnumerator<T>
|
|
{
|
|
private readonly RowNestedResultEnumerator _rowEnumerator;
|
|
private T _previousElement;
|
|
private State _state;
|
|
|
|
internal ObjectQueryNestedEnumerator(RowNestedResultEnumerator rowEnumerator)
|
|
{
|
|
_rowEnumerator = rowEnumerator;
|
|
_previousElement = default(T);
|
|
_state = State.Start;
|
|
}
|
|
|
|
public T Current { get { return _previousElement; } }
|
|
|
|
public void Dispose()
|
|
{
|
|
// Technically, calling GC.SuppressFinalize is not required because the class does not
|
|
// have a finalizer, but it does no harm, protects against the case where a finalizer is added
|
|
// in the future, and prevents an FxCop warning.
|
|
GC.SuppressFinalize(this);
|
|
_rowEnumerator.Dispose();
|
|
}
|
|
|
|
object System.Collections.IEnumerator.Current { get { return this.Current; } }
|
|
|
|
public bool MoveNext()
|
|
{
|
|
// See the documentation for enum State to understand the behaviors and requirements
|
|
// for each state.
|
|
switch (_state)
|
|
{
|
|
case State.Start:
|
|
{
|
|
if (TryReadToNextElement())
|
|
{
|
|
// if there's an element in the reader...
|
|
ReadElement();
|
|
}
|
|
else
|
|
{
|
|
// no data at all...
|
|
_state = State.NoRows;
|
|
}
|
|
};
|
|
break;
|
|
case State.Reading:
|
|
{
|
|
ReadElement();
|
|
};
|
|
break;
|
|
case State.NoRowsLastElementPending:
|
|
{
|
|
// nothing to do but move to the next state...
|
|
_state = State.NoRows;
|
|
};
|
|
break;
|
|
}
|
|
|
|
bool result;
|
|
if (_state == State.NoRows)
|
|
{
|
|
_previousElement = default(T);
|
|
result = false;
|
|
}
|
|
else
|
|
{
|
|
result = true;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Requires: the row is currently positioned at the start of an element.
|
|
///
|
|
/// Reads all rows in the element and sets up state for the next element (if any).
|
|
/// </summary>
|
|
private void ReadElement()
|
|
{
|
|
// remember the element we're currently reading
|
|
_previousElement = _rowEnumerator.RootCoordinator.Current;
|
|
|
|
// now we need to read to the next element (or the end of the
|
|
// reader) so that we can return the first element
|
|
if (TryReadToNextElement())
|
|
{
|
|
// we're positioned at the start of the next element (which
|
|
// corresponds to the 'reading' state)
|
|
_state = State.Reading;
|
|
}
|
|
else
|
|
{
|
|
// we're positioned at the end of the reader
|
|
_state = State.NoRowsLastElementPending;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads rows until the start of a new element is found. If no element
|
|
/// is found before all rows are consumed, returns false.
|
|
/// </summary>
|
|
private bool TryReadToNextElement()
|
|
{
|
|
while (_rowEnumerator.MoveNext())
|
|
{
|
|
// if we hit a new element, return true
|
|
if (_rowEnumerator.Current[0] != null)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
_rowEnumerator.Reset();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Describes the state of this enumerator with respect to the _rowEnumerator
|
|
/// it wraps.
|
|
/// </summary>
|
|
private enum State
|
|
{
|
|
/// <summary>
|
|
/// No rows have been read yet
|
|
/// </summary>
|
|
Start,
|
|
|
|
/// <summary>
|
|
/// Positioned at the start of a new root element. The previous element must
|
|
/// be stored in _previousElement. We read ahead in this manner so that
|
|
/// the previous element is fully populated (all of its children loaded)
|
|
/// before returning.
|
|
/// </summary>
|
|
Reading,
|
|
|
|
/// <summary>
|
|
/// Positioned past the end of the rows. The last element in the enumeration
|
|
/// has not yet been returned to the user however, and is stored in _previousElement.
|
|
/// </summary>
|
|
NoRowsLastElementPending,
|
|
|
|
/// <summary>
|
|
/// Positioned past the end of the rows. The last element has been returned to
|
|
/// the user.
|
|
/// </summary>
|
|
NoRows,
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wraps RowNestedResultEnumerator and yields results appropriate to an EntityReader instance. In particular,
|
|
/// yields RecordState whenever a new element becomes available at any depth in the result hierarchy.
|
|
/// </summary>
|
|
private class RecordStateEnumerator : IEnumerator<RecordState>
|
|
{
|
|
private readonly RowNestedResultEnumerator _rowEnumerator;
|
|
private RecordState _current;
|
|
|
|
/// <summary>
|
|
/// Gets depth of coordinator we're currently consuming. If _depth == -1, it means we haven't started
|
|
/// to consume the next row yet.
|
|
/// </summary>
|
|
private int _depth;
|
|
private bool _readerConsumed;
|
|
|
|
internal RecordStateEnumerator(RowNestedResultEnumerator rowEnumerator)
|
|
{
|
|
_rowEnumerator = rowEnumerator;
|
|
_current = null;
|
|
_depth = -1;
|
|
_readerConsumed = false;
|
|
}
|
|
|
|
public RecordState Current
|
|
{
|
|
get { return _current; }
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
// Technically, calling GC.SuppressFinalize is not required because the class does not
|
|
// have a finalizer, but it does no harm, protects against the case where a finalizer is added
|
|
// in the future, and prevents an FxCop warning.
|
|
GC.SuppressFinalize(this);
|
|
_rowEnumerator.Dispose();
|
|
}
|
|
|
|
object System.Collections.IEnumerator.Current
|
|
{
|
|
get { return _current; }
|
|
}
|
|
|
|
public bool MoveNext()
|
|
{
|
|
if (!_readerConsumed)
|
|
{
|
|
while (true)
|
|
{
|
|
// keep on cycling until we find a result
|
|
if (-1 == _depth || _rowEnumerator.Current.Length == _depth)
|
|
{
|
|
// time to move to the next row...
|
|
if (!_rowEnumerator.MoveNext())
|
|
{
|
|
// no more rows...
|
|
_current = null;
|
|
_readerConsumed = true;
|
|
break;
|
|
}
|
|
|
|
_depth = 0;
|
|
}
|
|
|
|
// check for results at the current depth
|
|
Coordinator currentCoordinator = _rowEnumerator.Current[_depth];
|
|
if (null != currentCoordinator)
|
|
{
|
|
_current = ((Coordinator<RecordState>)currentCoordinator).Current;
|
|
_depth++;
|
|
break;
|
|
}
|
|
|
|
_depth++;
|
|
}
|
|
}
|
|
|
|
return !_readerConsumed;
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
_rowEnumerator.Reset();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
}
|
|
}
|