|
//------------------------------------------------------------------------------
// <copyright file="EntityCommandDefinition.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//
// @owner Microsoft
// @backupOwner Microsoft
//------------------------------------------------------------------------------
namespace System.Data.EntityClient {
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Common.CommandTrees;
using System.Data.Common.Utils;
using System.Data.Mapping;
using System.Data.Metadata.Edm;
using System.Data.Query.InternalTrees;
using System.Data.Query.PlanCompiler;
using System.Data.Query.ResultAssembly;
using System.Diagnostics;
using System.Linq;
using System.Text;
/// <summary>
/// An aggregate Command Definition used by the EntityClient layers. This is an aggregator
/// object that represent information from multiple underlying provider commands.
/// </summary>
sealed internal class EntityCommandDefinition : DbCommandDefinition {
#region internal state
/// <summary>
/// nested store command definitions
/// </summary>
private readonly List<DbCommandDefinition> _mappedCommandDefinitions;
/// <summary>
/// generates column map for the store result reader
/// </summary>
private readonly IColumnMapGenerator[] _columnMapGenerators;
/// <summary>
/// list of the parameters that the resulting command should have
/// </summary>
private readonly System.Collections.ObjectModel.ReadOnlyCollection<EntityParameter> _parameters;
/// <summary>
/// Set of entity sets exposed in the command.
/// </summary>
private readonly Set<EntitySet> _entitySets;
#endregion
#region constructors
/// <summary>
/// don't let this be constructed publicly;
/// </summary>
/// <exception cref="EntityCommandCompilationException">Cannot prepare the command definition for execution; consult the InnerException for more information.</exception>
/// <exception cref="NotSupportedException">The ADO.NET Data Provider you are using does not support CommandTrees.</exception>
internal EntityCommandDefinition(DbProviderFactory storeProviderFactory, DbCommandTree commandTree) {
EntityUtil.CheckArgumentNull(storeProviderFactory, "storeProviderFactory");
EntityUtil.CheckArgumentNull(commandTree, "commandTree");
DbProviderServices storeProviderServices = DbProviderServices.GetProviderServices(storeProviderFactory);
try {
if (DbCommandTreeKind.Query == commandTree.CommandTreeKind) {
// Next compile the plan for the command tree
List<ProviderCommandInfo> mappedCommandList = new List<ProviderCommandInfo>();
ColumnMap columnMap;
int columnCount;
PlanCompiler.Compile(commandTree, out mappedCommandList, out columnMap, out columnCount, out _entitySets);
_columnMapGenerators = new IColumnMapGenerator[] {new ConstantColumnMapGenerator(columnMap, columnCount)};
// Note: we presume that the first item in the ProviderCommandInfo is the root node;
Debug.Assert(mappedCommandList.Count > 0, "empty providerCommandInfo collection and no exception?"); // this shouldn't ever happen.
// Then, generate the store commands from the resulting command tree(s)
_mappedCommandDefinitions = new List<DbCommandDefinition>(mappedCommandList.Count);
foreach (ProviderCommandInfo providerCommandInfo in mappedCommandList) {
DbCommandDefinition providerCommandDefinition = storeProviderServices.CreateCommandDefinition(providerCommandInfo.CommandTree);
if (null == providerCommandDefinition) {
throw EntityUtil.ProviderIncompatible(System.Data.Entity.Strings.ProviderReturnedNullForCreateCommandDefinition);
}
_mappedCommandDefinitions.Add(providerCommandDefinition);
}
}
else {
Debug.Assert(DbCommandTreeKind.Function == commandTree.CommandTreeKind, "only query and function command trees are supported");
DbFunctionCommandTree entityCommandTree = (DbFunctionCommandTree)commandTree;
// Retrieve mapping and metadata information for the function import.
FunctionImportMappingNonComposable mapping = GetTargetFunctionMapping(entityCommandTree);
IList<FunctionParameter> returnParameters = entityCommandTree.EdmFunction.ReturnParameters;
int resultSetCount = returnParameters.Count > 1 ? returnParameters.Count : 1;
_columnMapGenerators = new IColumnMapGenerator[resultSetCount];
TypeUsage storeResultType = DetermineStoreResultType(entityCommandTree.MetadataWorkspace, mapping, 0, out _columnMapGenerators[0]);
for (int i = 1; i < resultSetCount; i++)
{
DetermineStoreResultType(entityCommandTree.MetadataWorkspace, mapping, i, out _columnMapGenerators[i]);
}
// Copy over parameters (this happens through a more indirect route in the plan compiler, but
// it happens nonetheless)
List<KeyValuePair<string, TypeUsage>> providerParameters = new List<KeyValuePair<string, TypeUsage>>();
foreach (KeyValuePair<string, TypeUsage> parameter in entityCommandTree.Parameters)
{
providerParameters.Add(parameter);
}
// Construct store command tree usage.
DbFunctionCommandTree providerCommandTree = new DbFunctionCommandTree(entityCommandTree.MetadataWorkspace, DataSpace.SSpace,
mapping.TargetFunction, storeResultType, providerParameters);
DbCommandDefinition storeCommandDefinition = storeProviderServices.CreateCommandDefinition(providerCommandTree);
_mappedCommandDefinitions = new List<DbCommandDefinition>(1) { storeCommandDefinition };
EntitySet firstResultEntitySet = mapping.FunctionImport.EntitySets.FirstOrDefault();
if (firstResultEntitySet != null)
{
_entitySets = new Set<EntitySet>();
_entitySets.Add(mapping.FunctionImport.EntitySets.FirstOrDefault());
_entitySets.MakeReadOnly();
}
}
// Finally, build a list of the parameters that the resulting command should have;
List<EntityParameter> parameterList = new List<EntityParameter>();
foreach (KeyValuePair<string, TypeUsage> queryParameter in commandTree.Parameters) {
EntityParameter parameter = CreateEntityParameterFromQueryParameter(queryParameter);
parameterList.Add(parameter);
}
_parameters = new System.Collections.ObjectModel.ReadOnlyCollection<EntityParameter>(parameterList);
}
catch (EntityCommandCompilationException) {
// No need to re-wrap EntityCommandCompilationException
throw;
}
catch (Exception e) {
// we should not be wrapping all exceptions
if (EntityUtil.IsCatchableExceptionType(e)) {
// we don't wan't folks to have to know all the various types of exceptions that can
// occur, so we just rethrow a CommandDefinitionException and make whatever we caught
// the inner exception of it.
throw EntityUtil.CommandCompilation(System.Data.Entity.Strings.EntityClient_CommandDefinitionPreparationFailed, e);
}
throw;
}
}
/// <summary>
/// Determines the store type for a function import.
/// </summary>
private TypeUsage DetermineStoreResultType(MetadataWorkspace workspace, FunctionImportMappingNonComposable mapping, int resultSetIndex, out IColumnMapGenerator columnMapGenerator) {
// Determine column maps and infer result types for the mapped function. There are four varieties:
// Collection(Entity)
// Collection(PrimitiveType)
// Collection(ComplexType)
// No result type
TypeUsage storeResultType;
{
StructuralType baseStructuralType;
EdmFunction functionImport = mapping.FunctionImport;
// Collection(Entity) or Collection(ComplexType)
if (MetadataHelper.TryGetFunctionImportReturnType<StructuralType>(functionImport, resultSetIndex, out baseStructuralType))
{
ValidateEdmResultType(baseStructuralType, functionImport);
//Note: Defensive check for historic reasons, we expect functionImport.EntitySets.Count > resultSetIndex
EntitySet entitySet = functionImport.EntitySets.Count > resultSetIndex ? functionImport.EntitySets[resultSetIndex] : null;
columnMapGenerator = new FunctionColumnMapGenerator(mapping, resultSetIndex, entitySet, baseStructuralType);
// We don't actually know the return type for the stored procedure, but we can infer
// one based on the mapping (i.e.: a column for every property of the mapped types
// and for all discriminator columns)
storeResultType = mapping.GetExpectedTargetResultType(workspace, resultSetIndex);
}
// Collection(PrimitiveType)
else
{
FunctionParameter returnParameter = MetadataHelper.GetReturnParameter(functionImport, resultSetIndex);
if (returnParameter != null && returnParameter.TypeUsage != null)
{
// Get metadata description of the return type
storeResultType = returnParameter.TypeUsage;
Debug.Assert(storeResultType.EdmType.BuiltInTypeKind == BuiltInTypeKind.CollectionType, "FunctionImport currently supports only collection result type");
TypeUsage elementType = ((CollectionType)storeResultType.EdmType).TypeUsage;
Debug.Assert(Helper.IsScalarType(elementType.EdmType)
, "FunctionImport supports only Collection(Entity), Collection(Enum) and Collection(Primitive)");
// Build collection column map where the first column of the store result is assumed
// to contain the primitive type values.
ScalarColumnMap scalarColumnMap = new ScalarColumnMap(elementType, string.Empty, 0, 0);
SimpleCollectionColumnMap collectionColumnMap = new SimpleCollectionColumnMap(storeResultType,
string.Empty, scalarColumnMap, null, null);
columnMapGenerator = new ConstantColumnMapGenerator(collectionColumnMap, 1);
}
// No result type
else
{
storeResultType = null;
columnMapGenerator = new ConstantColumnMapGenerator(null, 0);
}
}
}
return storeResultType;
}
/// <summary>
/// Handles the following negative scenarios
/// Nested ComplexType Property in ComplexType
/// </summary>
/// <param name="resultType"></param>
private void ValidateEdmResultType(EdmType resultType, EdmFunction functionImport)
{
if (Helper.IsComplexType(resultType))
{
ComplexType complexType = resultType as ComplexType;
Debug.Assert(null != complexType, "we should have a complex type here");
foreach (var property in complexType.Properties)
{
if (property.TypeUsage.EdmType.BuiltInTypeKind == BuiltInTypeKind.ComplexType)
{
throw new NotSupportedException(System.Data.Entity.Strings.ComplexTypeAsReturnTypeAndNestedComplexProperty(property.Name, complexType.Name, functionImport.FullName));
}
}
}
}
/// <summary>
/// Retrieves mapping for the given C-Space functionCommandTree
/// </summary>
private static FunctionImportMappingNonComposable GetTargetFunctionMapping(DbFunctionCommandTree functionCommandTree)
{
Debug.Assert(functionCommandTree.DataSpace == DataSpace.CSpace, "map from CSpace->SSpace function");
Debug.Assert(functionCommandTree != null, "null functionCommandTree");
Debug.Assert(!functionCommandTree.EdmFunction.IsComposableAttribute, "functionCommandTree.EdmFunction must be non-composable.");
// Find mapped store function.
FunctionImportMapping targetFunctionMapping;
if (!functionCommandTree.MetadataWorkspace.TryGetFunctionImportMapping(functionCommandTree.EdmFunction, out targetFunctionMapping))
{
throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_UnmappedFunctionImport(functionCommandTree.EdmFunction.FullName));
}
return (FunctionImportMappingNonComposable)targetFunctionMapping;
}
#endregion
#region public API
/// <summary>
/// Create a DbCommand object from the definition, that can be executed
/// </summary>
/// <returns></returns>
public override DbCommand CreateCommand() {
return new EntityCommand(this);
}
#endregion
#region internal methods
/// <summary>
/// Get a list of commands to be executed by the provider
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal IEnumerable<string> MappedCommands {
get {
// Build up the list of command texts, if we haven't done so yet
List<string> mappedCommandTexts = new List<string>();
foreach (DbCommandDefinition commandDefinition in _mappedCommandDefinitions) {
DbCommand mappedCommand = commandDefinition.CreateCommand();
mappedCommandTexts.Add(mappedCommand.CommandText);
}
return mappedCommandTexts;
}
}
/// <summary>
/// Creates ColumnMap for result assembly using the given reader.
/// </summary>
internal ColumnMap CreateColumnMap(DbDataReader storeDataReader)
{
return CreateColumnMap(storeDataReader, 0);
}
/// <summary>
/// Creates ColumnMap for result assembly using the given reader's resultSetIndexth result set.
/// </summary>
internal ColumnMap CreateColumnMap(DbDataReader storeDataReader, int resultSetIndex)
{
return _columnMapGenerators[resultSetIndex].CreateColumnMap(storeDataReader);
}
/// <summary>
/// Property to expose the known parameters for the query, so the Command objects
/// constructor can poplulate it's parameter collection from.
/// </summary>
internal IEnumerable<EntityParameter> Parameters {
get {
return _parameters;
}
}
/// <summary>
/// Set of entity sets exposed in the command.
/// </summary>
internal Set<EntitySet> EntitySets {
get {
return _entitySets;
}
}
/// <summary>
/// Constructs a EntityParameter from a CQT parameter.
/// </summary>
/// <param name="queryParameter"></param>
/// <returns></returns>
private static EntityParameter CreateEntityParameterFromQueryParameter(KeyValuePair<string, TypeUsage> queryParameter) {
// We really can't have a parameter here that isn't a scalar type...
Debug.Assert(TypeSemantics.IsScalarType(queryParameter.Value), "Non-scalar type used as query parameter type");
EntityParameter result = new EntityParameter();
result.ParameterName = queryParameter.Key;
EntityCommandDefinition.PopulateParameterFromTypeUsage(result, queryParameter.Value, isOutParam: false);
return result;
}
internal static void PopulateParameterFromTypeUsage(EntityParameter parameter, TypeUsage type, bool isOutParam)
{
// type can be null here if the type provided by the user is not a known model type
if (type != null)
{
PrimitiveTypeKind primitiveTypeKind;
if (Helper.IsEnumType(type.EdmType))
{
type = TypeUsage.Create(Helper.GetUnderlyingEdmTypeForEnumType(type.EdmType));
}
else if (Helper.IsSpatialType(type, out primitiveTypeKind))
{
parameter.EdmType = EdmProviderManifest.Instance.GetPrimitiveType(primitiveTypeKind);
}
}
DbCommandDefinition.PopulateParameterFromTypeUsage(parameter, type, isOutParam);
}
/// <summary>
/// Internal execute method -- copies command information from the map command
/// to the command objects, executes them, and builds the result assembly
/// structures needed to return the data reader
/// </summary>
/// <param name="entityCommand"></param>
/// <param name="behavior"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException">behavior must specify CommandBehavior.SequentialAccess</exception>
/// <exception cref="InvalidOperationException">input parameters in the entityCommand.Parameters collection must have non-null values.</exception>
internal DbDataReader Execute(EntityCommand entityCommand, CommandBehavior behavior) {
if (CommandBehavior.SequentialAccess != (behavior & CommandBehavior.SequentialAccess)) {
throw EntityUtil.MustUseSequentialAccess();
}
DbDataReader storeDataReader = ExecuteStoreCommands(entityCommand, behavior);
DbDataReader result = null;
// If we actually executed something, then go ahead and construct a bridge
// data reader for it.
if (null != storeDataReader) {
try {
ColumnMap columnMap = this.CreateColumnMap(storeDataReader, 0);
if (null == columnMap) {
// For a query with no result type (and therefore no column map), consume the reader.
// When the user requests Metadata for this reader, we return nothing.
CommandHelper.ConsumeReader(storeDataReader);
result = storeDataReader;
}
else {
result = BridgeDataReader.Create(storeDataReader, columnMap, entityCommand.Connection.GetMetadataWorkspace(), GetNextResultColumnMaps(storeDataReader));
}
}
catch {
// dispose of store reader if there is an error creating the BridgeDataReader
storeDataReader.Dispose();
throw;
}
}
return result;
}
private IEnumerable<ColumnMap> GetNextResultColumnMaps(DbDataReader storeDataReader)
{
for (int i = 1; i < _columnMapGenerators.Length; ++i)
{
yield return this.CreateColumnMap(storeDataReader, i);
}
}
/// <summary>
/// Execute the store commands, and return IteratorSources for each one
/// </summary>
/// <param name="entityCommand"></param>
/// <param name="behavior"></param>
internal DbDataReader ExecuteStoreCommands(EntityCommand entityCommand, CommandBehavior behavior)
{
// SQLPT #120007433 is the work item to implement MARS support, which we
// need to do here, but since the PlanCompiler doesn't
// have it yet, neither do we...
if (1 != _mappedCommandDefinitions.Count) {
throw EntityUtil.NotSupported("MARS");
}
EntityTransaction entityTransaction = CommandHelper.GetEntityTransaction(entityCommand);
DbCommandDefinition definition = _mappedCommandDefinitions[0];
DbCommand storeProviderCommand = definition.CreateCommand();
CommandHelper.SetStoreProviderCommandState(entityCommand, entityTransaction, storeProviderCommand);
// Copy over the values from the map command to the store command; we
// assume that they were not renamed by either the plan compiler or SQL
// Generation.
//
// Note that this pretty much presumes that named parameters are supported
// by the store provider, but it might work if we don't reorder/reuse
// parameters.
//
// Note also that the store provider may choose to add parameters to thier
// command object for some things; we'll only copy over the values for
// parameters that we find in the EntityCommands parameters collection, so
// we won't damage anything the store provider did.
bool hasOutputParameters = false;
if (storeProviderCommand.Parameters != null) // SQLBUDT 519066
{
DbProviderServices storeProviderServices = DbProviderServices.GetProviderServices(entityCommand.Connection.StoreProviderFactory);
foreach (DbParameter storeParameter in storeProviderCommand.Parameters) {
// I could just use the string indexer, but then if I didn't find it the
// consumer would get some ParameterNotFound exeception message and that
// wouldn't be very meaningful. Instead, I use the IndexOf method and
// if I don't find it, it's not a big deal (The store provider must
// have added it).
int parameterOrdinal = entityCommand.Parameters.IndexOf(storeParameter.ParameterName);
if (-1 != parameterOrdinal) {
EntityParameter entityParameter = entityCommand.Parameters[parameterOrdinal];
SyncParameterProperties(entityParameter, storeParameter, storeProviderServices);
if (storeParameter.Direction != ParameterDirection.Input) {
hasOutputParameters = true;
}
}
}
}
// If the EntityCommand has output parameters, we must synchronize parameter values when
// the reader is closed. Tell the EntityCommand about the store command so that it knows
// where to pull those values from.
if (hasOutputParameters) {
entityCommand.SetStoreProviderCommand(storeProviderCommand);
}
DbDataReader reader = null;
try {
reader = storeProviderCommand.ExecuteReader(behavior & ~CommandBehavior.SequentialAccess);
}
catch (Exception e) {
// we should not be wrapping all exceptions
if (EntityUtil.IsCatchableExceptionType(e)) {
// we don't wan't folks to have to know all the various types of exceptions that can
// occur, so we just rethrow a CommandDefinitionException and make whatever we caught
// the inner exception of it.
throw EntityUtil.CommandExecution(System.Data.Entity.Strings.EntityClient_CommandDefinitionExecutionFailed, e);
}
throw;
}
return reader;
}
/// <summary>
/// Updates storeParameter size, precision and scale properties from user provided parameter properties.
/// </summary>
/// <param name="entityParameter"></param>
/// <param name="storeParameter"></param>
private static void SyncParameterProperties(EntityParameter entityParameter, DbParameter storeParameter, DbProviderServices storeProviderServices) {
IDbDataParameter dbDataParameter = (IDbDataParameter)storeParameter;
// DBType is not currently syncable; it's part of the cache key anyway; this is because we can't guarantee
// that the store provider will honor it -- (SqlClient doesn't...)
//if (entityParameter.IsDbTypeSpecified)
//{
// storeParameter.DbType = entityParameter.DbType;
//}
// Give the store provider the opportunity to set the value before any parameter state has been copied from
// the EntityParameter.
TypeUsage parameterTypeUsage = TypeHelpers.GetPrimitiveTypeUsageForScalar(entityParameter.GetTypeUsage());
storeProviderServices.SetParameterValue(storeParameter, parameterTypeUsage, entityParameter.Value);
// Override the store provider parameter state with any explicitly specified values from the EntityParameter.
if (entityParameter.IsDirectionSpecified)
{
storeParameter.Direction = entityParameter.Direction;
}
if (entityParameter.IsIsNullableSpecified)
{
storeParameter.IsNullable = entityParameter.IsNullable;
}
if (entityParameter.IsSizeSpecified)
{
storeParameter.Size = entityParameter.Size;
}
if (entityParameter.IsPrecisionSpecified)
{
dbDataParameter.Precision = entityParameter.Precision;
}
if (entityParameter.IsScaleSpecified)
{
dbDataParameter.Scale = entityParameter.Scale;
}
}
/// <summary>
/// Return the string used by EntityCommand and ObjectQuery<T> ToTraceString"/>
/// </summary>
/// <returns></returns>
internal string ToTraceString() {
if (_mappedCommandDefinitions != null) {
if (_mappedCommandDefinitions.Count == 1) {
// Gosh it sure would be nice if I could just get the inner commandText, but
// that would require more public surface area on DbCommandDefinition, or
// me to know about the inner object...
return _mappedCommandDefinitions[0].CreateCommand().CommandText;
}
else {
StringBuilder sb = new StringBuilder();
foreach (DbCommandDefinition commandDefinition in _mappedCommandDefinitions) {
DbCommand mappedCommand = commandDefinition.CreateCommand();
sb.Append(mappedCommand.CommandText);
}
return sb.ToString();
}
}
return string.Empty;
}
#endregion
#region nested types
/// <summary>
/// Generates a column map given a data reader.
/// </summary>
private interface IColumnMapGenerator {
/// <summary>
/// Given a data reader, returns column map.
/// </summary>
/// <param name="reader">Data reader.</param>
/// <returns>Column map.</returns>
ColumnMap CreateColumnMap(DbDataReader reader);
}
/// <summary>
/// IColumnMapGenerator wrapping a constant instance of a column map (invariant with respect
/// to the given DbDataReader)
/// </summary>
private sealed class ConstantColumnMapGenerator : IColumnMapGenerator {
private readonly ColumnMap _columnMap;
private readonly int _fieldsRequired;
internal ConstantColumnMapGenerator(ColumnMap columnMap, int fieldsRequired) {
_columnMap = columnMap;
_fieldsRequired = fieldsRequired;
}
ColumnMap IColumnMapGenerator.CreateColumnMap(DbDataReader reader) {
if (null != reader && reader.FieldCount < _fieldsRequired) {
throw EntityUtil.CommandExecution(System.Data.Entity.Strings.EntityClient_TooFewColumns);
}
return _columnMap;
}
}
/// <summary>
/// Generates column maps for a non-composable function mapping.
/// </summary>
private sealed class FunctionColumnMapGenerator : IColumnMapGenerator {
private readonly FunctionImportMappingNonComposable _mapping;
private readonly EntitySet _entitySet;
private readonly StructuralType _baseStructuralType;
private readonly int _resultSetIndex;
internal FunctionColumnMapGenerator(FunctionImportMappingNonComposable mapping, int resultSetIndex, EntitySet entitySet, StructuralType baseStructuralType)
{
_mapping = mapping;
_entitySet = entitySet;
_baseStructuralType = baseStructuralType;
_resultSetIndex = resultSetIndex;
}
ColumnMap IColumnMapGenerator.CreateColumnMap(DbDataReader reader)
{
return ColumnMapFactory.CreateFunctionImportStructuralTypeColumnMap(reader, _mapping, _resultSetIndex, _entitySet, _baseStructuralType);
}
}
#endregion
}
}
|