//--------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // // @owner [....] // @backupOwner [....] //--------------------------------------------------------------------- namespace System.Data.EntityClient { using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Data.Common; using System.Data.Common.CommandTrees; using System.Data.Common.CommandTrees.ExpressionBuilder; using System.Data.Common.EntitySql; using System.Data.Common.QueryCache; using System.Data.Common.Utils; using System.Data.Metadata.Edm; using System.Diagnostics; using System.Linq; /// /// Class representing a command for the conceptual layer /// public sealed class EntityCommand : DbCommand { #region Fields private const int InvalidCloseCount = -1; private bool _designTimeVisible; private string _esqlCommandText; private EntityConnection _connection; private DbCommandTree _preparedCommandTree; private EntityParameterCollection _parameters; private int? _commandTimeout; private CommandType _commandType; private EntityTransaction _transaction; private UpdateRowSource _updatedRowSource; private EntityCommandDefinition _commandDefinition; private bool _isCommandDefinitionBased; private DbCommandTree _commandTreeSetByUser; private DbDataReader _dataReader; private bool _enableQueryPlanCaching; private DbCommand _storeProviderCommand; #endregion /// /// Constructs the EntityCommand object not yet associated to a connection object /// public EntityCommand() { GC.SuppressFinalize(this); // Initalize the member field with proper default values this._designTimeVisible = true; this._commandType = CommandType.Text; this._updatedRowSource = UpdateRowSource.Both; this._parameters = new EntityParameterCollection(); // Future Enhancement: (See SQLPT #300004256) At some point it would be // really nice to read defaults from a global configuration, but we're not // doing that today. this._enableQueryPlanCaching = true; } /// /// Constructs the EntityCommand object with the given eSQL statement, but not yet associated to a connection object /// /// The eSQL command text to execute public EntityCommand(string statement) : this() { // Assign other member fields from the parameters this._esqlCommandText = statement; } /// /// Constructs the EntityCommand object with the given eSQL statement and the connection object to use /// /// The eSQL command text to execute /// The connection object public EntityCommand(string statement, EntityConnection connection) : this(statement) { // Assign other member fields from the parameters this._connection = connection; } /// /// Constructs the EntityCommand object with the given eSQL statement and the connection object to use /// /// The eSQL command text to execute /// The connection object /// The transaction object this command executes in public EntityCommand(string statement, EntityConnection connection, EntityTransaction transaction) : this(statement, connection) { // Assign other member fields from the parameters this._transaction = transaction; } /// /// Internal constructor used by EntityCommandDefinition /// /// The prepared command definition that can be executed using this EntityCommand internal EntityCommand(EntityCommandDefinition commandDefinition) : this() { // Assign other member fields from the parameters this._commandDefinition = commandDefinition; this._parameters = new EntityParameterCollection(); // Make copies of the parameters foreach (EntityParameter parameter in commandDefinition.Parameters) { this._parameters.Add(parameter.Clone()); } // Reset the dirty flag that was set to true when the parameters were added so that it won't say // it's dirty to start with this._parameters.ResetIsDirty(); // Track the fact that this command was created from and represents an already prepared command definition this._isCommandDefinitionBased = true; } /// /// Constructs a new EntityCommand given a EntityConnection and an EntityCommandDefition. This /// constructor is used by ObjectQueryExecution plan to execute an ObjectQuery. /// /// The connection against which this EntityCommand should execute /// The prepared command definition that can be executed using this EntityCommand internal EntityCommand(EntityConnection connection, EntityCommandDefinition entityCommandDefinition ) : this(entityCommandDefinition) { this._connection = connection; } /// /// The connection object used for executing the command /// public new EntityConnection Connection { get { return this._connection; } set { ThrowIfDataReaderIsOpen(); if (this._connection != value) { if (null != this._connection) { Unprepare(); } this._connection = value; this._transaction = null; } } } /// /// The connection object used for executing the command /// protected override DbConnection DbConnection { get { return this.Connection; } set { this.Connection = (EntityConnection)value; } } /// /// The eSQL statement to execute, only one of the command tree or the command text can be set, not both /// public override string CommandText { get { // If the user set the command tree previously, then we cannot retrieve the command text if (this._commandTreeSetByUser != null) throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_CannotGetCommandText); return this._esqlCommandText ?? ""; } set { ThrowIfDataReaderIsOpen(); // If the user set the command tree previously, then we cannot set the command text if (this._commandTreeSetByUser != null) throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_CannotSetCommandText); if (this._esqlCommandText != value) { this._esqlCommandText = value; // Wipe out any preparation work we have done Unprepare(); // If the user-defined command text or tree has been set (even to null or empty), // then this command can no longer be considered command definition-based this._isCommandDefinitionBased = false; } } } /// /// The command tree to execute, only one of the command tree or the command text can be set, not both. /// public DbCommandTree CommandTree { get { // If the user set the command text previously, then we cannot retrieve the command tree if (!string.IsNullOrEmpty(this._esqlCommandText)) throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_CannotGetCommandTree); return this._commandTreeSetByUser; } set { ThrowIfDataReaderIsOpen(); // If the user set the command text previously, then we cannot set the command tree if (!string.IsNullOrEmpty(this._esqlCommandText)) throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_CannotSetCommandTree); // If the command type is not Text, CommandTree cannot be set if (CommandType.Text != CommandType) { throw EntityUtil.InternalError(EntityUtil.InternalErrorCode.CommandTreeOnStoredProcedureEntityCommand); } if (this._commandTreeSetByUser != value) { this._commandTreeSetByUser = value; // Wipe out any preparation work we have done Unprepare(); // If the user-defined command text or tree has been set (even to null or empty), // then this command can no longer be considered command definition-based this._isCommandDefinitionBased = false; } } } /// /// Get or set the time in seconds to wait for the command to execute /// public override int CommandTimeout { get { // Returns the timeout value if it has been set if (this._commandTimeout != null) { return this._commandTimeout.Value; } // Create a provider command object just so we can ask the default timeout if (this._connection != null && this._connection.StoreProviderFactory != null) { DbCommand storeCommand = this._connection.StoreProviderFactory.CreateCommand(); if (storeCommand != null) { return storeCommand.CommandTimeout; } } return 0; } set { ThrowIfDataReaderIsOpen(); this._commandTimeout = value; } } /// /// The type of command being executed, only applicable when the command is using an eSQL statement and not the tree /// public override CommandType CommandType { get { return this._commandType; } set { ThrowIfDataReaderIsOpen(); // For now, command type other than Text is not supported if (value != CommandType.Text && value != CommandType.StoredProcedure) { throw EntityUtil.NotSupported(System.Data.Entity.Strings.EntityClient_UnsupportedCommandType); } this._commandType = value; } } /// /// The collection of parameters for this command /// public new EntityParameterCollection Parameters { get { return this._parameters; } } /// /// The collection of parameters for this command /// protected override DbParameterCollection DbParameterCollection { get { return this.Parameters; } } /// /// The transaction object used for executing the command /// public new EntityTransaction Transaction { get { return this._transaction; // SQLBU 496829 } set { ThrowIfDataReaderIsOpen(); this._transaction = value; } } /// /// The transaction that this command executes in /// protected override DbTransaction DbTransaction { get { return this.Transaction; } set { this.Transaction = (EntityTransaction)value; } } /// /// Gets or sets how command results are applied to the DataRow when used by the Update method of a DbDataAdapter /// public override UpdateRowSource UpdatedRowSource { get { return this._updatedRowSource; } set { ThrowIfDataReaderIsOpen(); this._updatedRowSource = value; } } /// /// Hidden property used by the designers /// public override bool DesignTimeVisible { get { return this._designTimeVisible; } set { ThrowIfDataReaderIsOpen(); this._designTimeVisible = value; TypeDescriptor.Refresh(this); } } /// /// Enables/Disables query plan caching for this EntityCommand /// public bool EnablePlanCaching { get { return this._enableQueryPlanCaching; } set { ThrowIfDataReaderIsOpen(); this._enableQueryPlanCaching = value; } } /// /// Cancel the execution of the command /// public override void Cancel() { } /// /// Create and return a new parameter object representing a parameter in the eSQL statement /// /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] public new EntityParameter CreateParameter() { return new EntityParameter(); } /// /// Create and return a new parameter object representing a parameter in the eSQL statement /// protected override DbParameter CreateDbParameter() { return CreateParameter(); } /// /// Executes the command and returns a data reader for reading the results /// /// A data readerobject public new EntityDataReader ExecuteReader() { return ExecuteReader(CommandBehavior.Default); } /// /// Executes the command and returns a data reader for reading the results. May only /// be called on CommandType.CommandText (otherwise, use the standard Execute* methods) /// /// The behavior to use when executing the command /// A data readerobject /// For stored procedure commands, if called /// for anything but an entity collection result public new EntityDataReader ExecuteReader(CommandBehavior behavior) { Prepare(); // prepare the query first EntityDataReader reader = new EntityDataReader(this, _commandDefinition.Execute(this, behavior), behavior); _dataReader = reader; return reader; } /// /// Executes the command and returns a data reader for reading the results /// /// The behavior to use when executing the command /// A data readerobject protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) { return ExecuteReader(behavior); } /// /// Executes the command and discard any results returned from the command /// /// Number of rows affected public override int ExecuteNonQuery() { return ExecuteScalar(reader => { // consume reader before checking records affected CommandHelper.ConsumeReader(reader); return reader.RecordsAffected; }); } /// /// Executes the command and return the first column in the first row of the result, extra results are ignored /// /// The result in the first column in the first row public override object ExecuteScalar() { return ExecuteScalar(reader => { object result = reader.Read() ? reader.GetValue(0) : null; // consume reader before retrieving parameters CommandHelper.ConsumeReader(reader); return result; }); } /// /// Executes a reader and retrieves a scalar value using the given resultSelector delegate /// private T_Result ExecuteScalar(Func resultSelector) { T_Result result; using (EntityDataReader reader = ExecuteReader(CommandBehavior.SequentialAccess)) { result = resultSelector(reader); } return result; } /// /// Clear out any "compile" state /// internal void Unprepare() { this._commandDefinition = null; this._preparedCommandTree = null; // Clear the dirty flag on the parameters and parameter collection _parameters.ResetIsDirty(); } /// /// Creates a prepared version of this command /// public override void Prepare() { ThrowIfDataReaderIsOpen(); CheckIfReadyToPrepare(); InnerPrepare(); } /// /// Creates a prepared version of this command without regard to the current connection state. /// Called by both and . /// private void InnerPrepare() { // Unprepare if the parameters have changed to force a reprepare if (_parameters.IsDirty) { Unprepare(); } _commandDefinition = GetCommandDefinition(); Debug.Assert(null != _commandDefinition, "_commandDefinition cannot be null"); } /// /// Ensures we have the command tree, either the user passed us the tree, or an eSQL statement that we need to parse /// private void MakeCommandTree() { // We must have a connection before we come here Debug.Assert(this._connection != null); // Do the work only if we don't have a command tree yet if (this._preparedCommandTree == null) { DbCommandTree resultTree = null; if (this._commandTreeSetByUser != null) { resultTree = this._commandTreeSetByUser; } else if (CommandType.Text == CommandType) { if (!string.IsNullOrEmpty(this._esqlCommandText)) { // The perspective to be used for the query compilation Perspective perspective = (Perspective)new ModelPerspective(_connection.GetMetadataWorkspace()); // get a dictionary of names and typeusage from entity parameter collection Dictionary queryParams = GetParameterTypeUsage(); resultTree = CqlQuery.Compile( this._esqlCommandText, perspective, null /*parser option - use default*/, queryParams.Select(paramInfo => paramInfo.Value.Parameter(paramInfo.Key))).CommandTree; } else { // We have no command text, no command tree, so throw an exception if (this._isCommandDefinitionBased) { // This command was based on a prepared command definition and has no command text, // so reprepare is not possible. To create a new command with different parameters // requires creating a new entity command definition and calling it's CreateCommand method. throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_CannotReprepareCommandDefinitionBasedCommand); } else { throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_NoCommandText); } } } else if (CommandType.StoredProcedure == CommandType) { // get a dictionary of names and typeusage from entity parameter collection IEnumerable> queryParams = GetParameterTypeUsage(); EdmFunction function = DetermineFunctionImport(); resultTree = new DbFunctionCommandTree(this.Connection.GetMetadataWorkspace(), DataSpace.CSpace, function, null, queryParams); } // After everything is good and succeeded, assign the result to our field this._preparedCommandTree = resultTree; } } // requires: this must be a StoreProcedure command // effects: determines the EntityContainer function import referenced by this.CommandText private EdmFunction DetermineFunctionImport() { Debug.Assert(CommandType.StoredProcedure == this.CommandType); if (string.IsNullOrEmpty(this.CommandText) || string.IsNullOrEmpty(this.CommandText.Trim())) { throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_FunctionImportEmptyCommandText); } MetadataWorkspace workspace = _connection.GetMetadataWorkspace(); // parse the command text string containerName; string functionImportName; string defaultContainerName = null; // no default container in EntityCommand CommandHelper.ParseFunctionImportCommandText(this.CommandText, defaultContainerName, out containerName, out functionImportName); return CommandHelper.FindFunctionImport(_connection.GetMetadataWorkspace(), containerName, functionImportName); } /// /// Get the command definition for the command; will construct one if there is not already /// one constructed, which means it will prepare the command on the client. /// /// the command definition internal EntityCommandDefinition GetCommandDefinition() { EntityCommandDefinition entityCommandDefinition = _commandDefinition; // Construct the command definition using no special options; if (null == entityCommandDefinition) { // // check if the _commandDefinition is in cache // if (!TryGetEntityCommandDefinitionFromQueryCache(out entityCommandDefinition)) { // // if not, construct the command definition using no special options; // entityCommandDefinition = CreateCommandDefinition(); } _commandDefinition = entityCommandDefinition; } return entityCommandDefinition; } /// /// Returns the store command text. /// /// [Browsable(false)] public string ToTraceString() { CheckConnectionPresent(); InnerPrepare(); EntityCommandDefinition commandDefinition = _commandDefinition; if (null != commandDefinition) { return commandDefinition.ToTraceString(); } return string.Empty; } /// /// Gets an entitycommanddefinition from cache if a match is found for the given cache key. /// /// out param. returns the entitycommanddefinition for a given cache key /// true if a match is found in cache, false otherwise private bool TryGetEntityCommandDefinitionFromQueryCache( out EntityCommandDefinition entityCommandDefinition ) { Debug.Assert(null != _connection, "Connection must not be null at this point"); entityCommandDefinition = null; // // if EnableQueryCaching is false, then just return to force the CommandDefinition to be created // if (!this._enableQueryPlanCaching || string.IsNullOrEmpty(this._esqlCommandText)) { return false; } // // Create cache key // EntityClientCacheKey queryCacheKey = new EntityClientCacheKey(this); // // Try cache lookup // QueryCacheManager queryCacheManager = _connection.GetMetadataWorkspace().GetQueryCacheManager(); Debug.Assert(null != queryCacheManager,"QuerycacheManager instance cannot be null"); if (!queryCacheManager.TryCacheLookup(queryCacheKey, out entityCommandDefinition)) { // // if not, construct the command definition using no special options; // entityCommandDefinition = CreateCommandDefinition(); // // add to the cache // QueryCacheEntry outQueryCacheEntry = null; if (queryCacheManager.TryLookupAndAdd(new QueryCacheEntry(queryCacheKey, entityCommandDefinition), out outQueryCacheEntry)) { entityCommandDefinition = (EntityCommandDefinition)outQueryCacheEntry.GetTarget(); } } Debug.Assert(null != entityCommandDefinition, "out entityCommandDefinition must not be null"); return true; } /// /// Creates a commandDefinition for the command, using the options specified. /// /// Note: This method must not be side-effecting of the command /// /// the command definition private EntityCommandDefinition CreateCommandDefinition() { MakeCommandTree(); // Always check the CQT metadata against the connection metadata (internally, CQT already // validates metadata consistency) if (!_preparedCommandTree.MetadataWorkspace.IsMetadataWorkspaceCSCompatible(this.Connection.GetMetadataWorkspace())) { throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_CommandTreeMetadataIncompatible); } EntityCommandDefinition result = EntityProviderServices.Instance.CreateCommandDefinition(this._connection.StoreProviderFactory, this._preparedCommandTree); return result; } private void CheckConnectionPresent() { if (this._connection == null) { throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_NoConnectionForCommand); } } /// /// Checking the integrity of this command object to see if it's ready to be prepared or executed /// private void CheckIfReadyToPrepare() { // Check that we have a connection CheckConnectionPresent(); if (this._connection.StoreProviderFactory == null || this._connection.StoreConnection == null) { throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_ConnectionStringNeededBeforeOperation); } // Make sure the connection is not closed or broken if ((this._connection.State == ConnectionState.Closed) || (this._connection.State == ConnectionState.Broken)) { string message = System.Data.Entity.Strings.EntityClient_ExecutingOnClosedConnection( this._connection.State == ConnectionState.Closed ? System.Data.Entity.Strings.EntityClient_ConnectionStateClosed : System.Data.Entity.Strings.EntityClient_ConnectionStateBroken); throw EntityUtil.InvalidOperation(message); } } /// /// Checking if the command is still tied to a data reader, if so, then the reader must still be open and we throw /// private void ThrowIfDataReaderIsOpen() { if (this._dataReader != null) { throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_DataReaderIsStillOpen); } } /// /// Returns a dictionary of parameter name and parameter typeusage in s-space from the entity parameter /// collection given by the user. /// /// internal Dictionary GetParameterTypeUsage() { Debug.Assert(null != _parameters, "_parameters must not be null"); // Extract type metadata objects from the parameters to be used by CqlQuery.Compile Dictionary queryParams = new Dictionary(_parameters.Count); foreach (EntityParameter parameter in this._parameters) { // Validate that the parameter name has the format: A character followed by alphanumerics or // underscores string parameterName = parameter.ParameterName; if (string.IsNullOrEmpty(parameterName)) { throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_EmptyParameterName); } // Check each parameter to make sure it's an input parameter, currently EntityCommand doesn't support // anything else if (this.CommandType == CommandType.Text && parameter.Direction != ParameterDirection.Input) { throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_InvalidParameterDirection(parameter.ParameterName)); } // Checking that we can deduce the type from the parameter if the type is not set if (parameter.EdmType == null && parameter.DbType == DbType.Object && (parameter.Value == null || parameter.Value is DBNull)) { throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_UnknownParameterType(parameterName)); } // Validate that the parameter has an appropriate type and value // Any failures in GetTypeUsage will be surfaced as exceptions to the user TypeUsage typeUsage = null; typeUsage = parameter.GetTypeUsage(); // Add the query parameter, add the same time detect if this parameter has the same name of a previous parameter try { queryParams.Add(parameterName, typeUsage); } catch (ArgumentException e) { throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_DuplicateParameterNames(parameter.ParameterName), e); } } return queryParams; } /// /// Call only when the reader associated with this command is closing. Copies parameter values where necessary. /// internal void NotifyDataReaderClosing() { // Disassociating the data reader with this command this._dataReader = null; if (null != _storeProviderCommand) { CommandHelper.SetEntityParameterValues(this, _storeProviderCommand, _connection); _storeProviderCommand = null; } if (null != this.OnDataReaderClosing) { this.OnDataReaderClosing(this, new EventArgs()); } } /// /// Tells the EntityCommand about the underlying store provider command in case it needs to pull parameter values /// when the reader is closing. /// internal void SetStoreProviderCommand(DbCommand storeProviderCommand) { _storeProviderCommand = storeProviderCommand; } /// /// Event raised when the reader is closing. /// internal event EventHandler OnDataReaderClosing; } }