diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 383c97cd6c..8ac17c7495 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -573,6 +573,9 @@ Microsoft\Data\SqlClient\SqlColumnEncryptionKeyStoreProvider.cs + + Microsoft\Data\SqlClient\SqlCommand.cs + Microsoft\Data\SqlClient\SqlCommandSet.cs @@ -808,7 +811,7 @@ System\Diagnostics\CodeAnalysis.cs - + diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs similarity index 96% rename from src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs rename to src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index 236cb8d888..d43b71ff32 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -26,16 +26,10 @@ // New attributes that are designed to work with Microsoft.Data.SqlClient and are publicly documented should be included in future. namespace Microsoft.Data.SqlClient { - /// - [DefaultEvent("RecordsAffected")] - [ToolboxItem(true)] - [DesignerCategory("")] // TODO: Add designer attribute when Microsoft.VSDesigner.Data.VS.SqlCommandDesigner uses Microsoft.Data.SqlClient public sealed partial class SqlCommand : DbCommand, ICloneable { - private static int _objectTypeCount; // EventSource Counter private const int MaxRPCNameLength = 1046; - internal readonly int ObjectID = Interlocked.Increment(ref _objectTypeCount); internal sealed class ExecuteReaderAsyncCallContext : AAsyncCallContext { @@ -113,8 +107,6 @@ protected override void AfterCleared(SqlCommand owner) } } - private string _commandText; - private CommandType _commandType; private int? _commandTimeout; private UpdateRowSource _updatedRowSource = UpdateRowSource.Both; private bool _designTimeInvisible; @@ -165,38 +157,10 @@ protected override void AfterCleared(SqlCommand owner) internal static readonly Action s_cancelIgnoreFailure = CancelIgnoreFailureCallback; - // Prepare - // Against 7.0 Serve a prepare/unprepare requires an extra roundtrip to the server. - // - // From 8.0 and above, the preparation can be done as part of the command execution. - - private enum EXECTYPE - { - UNPREPARED, // execute unprepared commands, all server versions (results in sp_execsql call) - PREPAREPENDING, // prepare and execute command, 8.0 and above only (results in sp_prepexec call) - PREPARED, // execute prepared commands, all server versions (results in sp_exec call) - } - - // _hiddenPrepare - // On 8.0 and above the Prepared state cannot be left. Once a command is prepared it will always be prepared. - // A change in parameters, commandtext etc (IsDirty) automatically causes a hidden prepare - // - // _inPrepare will be set immediately before the actual prepare is done. - // The OnReturnValue function will test this flag to determine whether the returned value is a _prepareHandle or something else. - // - // _prepareHandle - the handle of a prepared command. Apparently there can be multiple prepared commands at a time - a feature that we do not support yet. - - private static readonly object s_cachedInvalidPrepareHandle = (object)-1; - private bool _inPrepare = false; - private object _prepareHandle = s_cachedInvalidPrepareHandle; // this is an int which is used in the object typed SqlParameter.Value field, avoid repeated boxing by storing in a box - private bool _hiddenPrepare = false; private int _preparedConnectionCloseCount = -1; private int _preparedConnectionReconnectCount = -1; private SqlParameterCollection _parameters; - private SqlConnection _activeConnection; - private bool _dirty = false; // true if the user changes the commandtext or number of parameters after the command is already prepared - private EXECTYPE _execType = EXECTYPE.UNPREPARED; // by default, assume the user is not sharing a connection so the command has not been prepared private _SqlRPC[] _rpcArrayOf1 = null; // Used for RPC executes private _SqlRPC _rpcForEncryption = null; // Used for sp_describe_parameter_encryption RPC executes @@ -227,13 +191,6 @@ private bool ShouldCacheEncryptionMetadata #if DEBUG internal static int DebugForceAsyncWriteDelay { get; set; } #endif - internal bool InPrepare - { - get - { - return _inPrepare; - } - } /// /// Return if column encryption setting is enabled. @@ -382,8 +339,6 @@ private AsyncState CachedAsyncState private StatementCompletedEventHandler _statementCompletedEventHandler; - private TdsParserStateObject _stateObj; // this is the TDS session we're using. - // Volatile bool used to synchronize with cancel thread the state change of an executing // command going from pre-processing to obtaining a stateObject. The cancel synchronization // we require in the command is only from entering an Execute* API to obtaining a @@ -880,131 +835,6 @@ private void PropertyChanging() this.IsDirty = true; } - /// - public override void Prepare() - { - // Reset _pendingCancel upon entry into any Execute - used to synchronize state - // between entry into Execute* API and the thread obtaining the stateObject. - _pendingCancel = false; - - SqlStatistics statistics = null; - using (TryEventScope.Create("SqlCommand.Prepare | API | Object Id {0}", ObjectID)) - { - SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.Prepare | API | Correlation | Object Id {0}, ActivityID {1}, Client Connection Id {2}", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId); - try - { - statistics = SqlStatistics.StartTimer(Statistics); - - // only prepare if batch with parameters - if (this.IsPrepared && !this.IsDirty - || (this.CommandType == CommandType.StoredProcedure) - || ((System.Data.CommandType.Text == this.CommandType) - && (0 == GetParameterCount(_parameters)))) - { - if (Statistics != null) - { - Statistics.SafeIncrement(ref Statistics._prepares); - } - _hiddenPrepare = false; - } - else - { - // Validate the command outside of the try/catch to avoid putting the _stateObj on error - ValidateCommand(isAsync: false); - - bool processFinallyBlock = true; - try - { - // NOTE: The state object isn't actually needed for this, but it is still here for back-compat (since it does a bunch of checks) - GetStateObject(); - - // Loop through parameters ensuring that we do not have unspecified types, sizes, scales, or precisions - if (_parameters != null) - { - int count = _parameters.Count; - for (int i = 0; i < count; ++i) - { - _parameters[i].Prepare(this); - } - } - - InternalPrepare(); - } - catch (Exception e) - { - processFinallyBlock = ADP.IsCatchableExceptionType(e); - throw; - } - finally - { - if (processFinallyBlock) - { - _hiddenPrepare = false; // The command is now officially prepared - - ReliablePutStateObject(); - } - } - } - } - finally - { - SqlStatistics.StopTimer(statistics); - } - } - } - - private void InternalPrepare() - { - if (this.IsDirty) - { - Debug.Assert(_cachedMetaData == null || !_dirty, "dirty query should not have cached metadata!"); // can have cached metadata if dirty because of parameters - // - // someone changed the command text or the parameter schema so we must unprepare the command - // - this.Unprepare(); - this.IsDirty = false; - } - Debug.Assert(_execType != EXECTYPE.PREPARED, "Invalid attempt to Prepare already Prepared command!"); - Debug.Assert(_activeConnection != null, "must have an open connection to Prepare"); - Debug.Assert(_stateObj != null, "TdsParserStateObject should not be null"); - Debug.Assert(_stateObj.Parser != null, "TdsParser class should not be null in Command.Execute!"); - Debug.Assert(_stateObj.Parser == _activeConnection.Parser, "stateobject parser not same as connection parser"); - Debug.Assert(false == _inPrepare, "Already in Prepare cycle, this.inPrepare should be false!"); - - // remember that the user wants to do a prepare but don't actually do an rpc - _execType = EXECTYPE.PREPAREPENDING; - // Note the current close count of the connection - this will tell us if the connection has been closed between calls to Prepare() and Execute - _preparedConnectionCloseCount = _activeConnection.CloseCount; - _preparedConnectionReconnectCount = _activeConnection.ReconnectCount; - - if (Statistics != null) - { - Statistics.SafeIncrement(ref Statistics._prepares); - } - } - - // SqlInternalConnectionTds needs to be able to unprepare a statement - internal void Unprepare() - { - Debug.Assert(true == IsPrepared, "Invalid attempt to Unprepare a non-prepared command!"); - Debug.Assert(_activeConnection != null, "must have an open connection to UnPrepare"); - Debug.Assert(false == _inPrepare, "_inPrepare should be false!"); - _execType = EXECTYPE.PREPAREPENDING; - - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.UnPrepare | Info | Object Id {0}, Current Prepared Handle {1}", ObjectID, _prepareHandle); - - // Don't zero out the handle because we'll pass it in to sp_prepexec on the next prepare - // Unless the close count isn't the same as when we last prepared - if ((_activeConnection.CloseCount != _preparedConnectionCloseCount) || (_activeConnection.ReconnectCount != _preparedConnectionReconnectCount)) - { - // reset our handle - _prepareHandle = s_cachedInvalidPrepareHandle; - } - - _cachedMetaData = null; - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.UnPrepare | Info | Object Id {0}, Command unprepared.", ObjectID); - } - // Cancel is supposed to be multi-thread safe. // It doesn't make sense to verify the connection exists or that it is open during cancel // because immediately after checking the connection can be closed or removed via another thread. @@ -6761,48 +6591,6 @@ internal void OnConnectionClosed() } } - internal TdsParserStateObject StateObject - { - get - { - return _stateObj; - } - } - - private bool IsPrepared - { - get { return (_execType != EXECTYPE.UNPREPARED); } - } - - private bool IsUserPrepared - { - get { return IsPrepared && !_hiddenPrepare && !IsDirty; } - } - - internal bool IsDirty - { - get - { - // only dirty if prepared - var activeConnection = _activeConnection; - return (IsPrepared && - (_dirty || - ((_parameters != null) && (_parameters.IsDirty)) || - ((activeConnection != null) && ((activeConnection.CloseCount != _preparedConnectionCloseCount) || (activeConnection.ReconnectCount != _preparedConnectionReconnectCount))))); - } - set - { - // only mark the command as dirty if it is already prepared - // but always clear the value if it we are clearing the dirty flag - _dirty = value ? IsPrepared : false; - if (_parameters != null) - { - _parameters.IsDirty = _dirty; - } - _cachedMetaData = null; - } - } - /// /// Get or add to the number of records affected by SpDescribeParameterEncryption. /// The below line is used only for debug asserts and not exposed publicly or impacts functionality otherwise. diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index b3231b0cee..3616086d00 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -684,6 +684,9 @@ Microsoft\Data\SqlClient\SqlColumnEncryptionKeyStoreProvider.cs + + Microsoft\Data\SqlClient\SqlCommand.cs + Microsoft\Data\SqlClient\SqlCommandBuilder.cs @@ -919,7 +922,7 @@ System\Runtime\CompilerServices\IsExternalInit.netfx.cs - + diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs similarity index 96% rename from src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs rename to src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index b3339f9c50..6e986435bc 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -29,16 +29,10 @@ // New attributes that are designed to work with Microsoft.Data.SqlClient and are publicly documented should be included in future. namespace Microsoft.Data.SqlClient { - /// - [DefaultEvent("RecordsAffected")] - [ToolboxItem(true)] - [DesignerCategory("")] // TODO: Add designer attribute when Microsoft.VSDesigner.Data.VS.SqlCommandDesigner uses Microsoft.Data.SqlClient - public sealed class SqlCommand : DbCommand, ICloneable + public sealed partial class SqlCommand : DbCommand, ICloneable { - private static int _objectTypeCount; // EventSource Counter private const int MaxRPCNameLength = 1046; - internal readonly int ObjectID = Interlocked.Increment(ref _objectTypeCount); internal sealed class ExecuteReaderAsyncCallContext : AAsyncCallContext { @@ -116,8 +110,6 @@ protected override void AfterCleared(SqlCommand owner) } } - private string _commandText; - private CommandType _commandType; private int? _commandTimeout; private UpdateRowSource _updatedRowSource = UpdateRowSource.Both; private bool _designTimeInvisible; @@ -164,40 +156,10 @@ protected override void AfterCleared(SqlCommand owner) #endif internal static readonly Action s_cancelIgnoreFailure = CancelIgnoreFailureCallback; - // Prepare - // Against 7.0 Serve a prepare/unprepare requires an extra roundtrip to the server. - // - // From 8.0 and above, the preparation can be done as part of the command execution. - - private enum EXECTYPE - { - UNPREPARED, // execute unprepared commands, all server versions (results in sp_execsql call) - PREPAREPENDING, // prepare and execute command, 8.0 and above only (results in sp_prepexec call) - PREPARED, // execute prepared commands, all server versions (results in sp_exec call) - } - - // devnotes - // - // _hiddenPrepare - // On 8.0 and above the Prepared state cannot be left. Once a command is prepared it will always be prepared. - // A change in parameters, commandtext etc (IsDirty) automatically causes a hidden prepare - // - // _inPrepare will be set immediately before the actual prepare is done. - // The OnReturnValue function will test this flag to determine whether the returned value is a _prepareHandle or something else. - // - // _prepareHandle - the handle of a prepared command. Apparently there can be multiple prepared commands at a time - a feature that we do not support yet. - - private static readonly object s_cachedInvalidPrepareHandle = (object)-1; - private bool _inPrepare = false; - private object _prepareHandle = s_cachedInvalidPrepareHandle; // this is an int which is used in the object typed SqlParameter.Value field, avoid repeated boxing by storing in a box - private bool _hiddenPrepare = false; private int _preparedConnectionCloseCount = -1; private int _preparedConnectionReconnectCount = -1; private SqlParameterCollection _parameters; - private SqlConnection _activeConnection; - private bool _dirty = false; // true if the user changes the commandtext or number of parameters after the command is already prepared - private EXECTYPE _execType = EXECTYPE.UNPREPARED; // by default, assume the user is not sharing a connection so the command has not been prepared private _SqlRPC[] _rpcArrayOf1 = null; // Used for RPC executes private _SqlRPC _rpcForEncryption = null; // Used for sp_describe_parameter_encryption RPC executes @@ -228,13 +190,6 @@ private bool ShouldCacheEncryptionMetadata #if DEBUG internal static int DebugForceAsyncWriteDelay { get; set; } #endif - internal bool InPrepare - { - get - { - return _inPrepare; - } - } /// /// Return if column encryption setting is enabled. @@ -384,8 +339,6 @@ private AsyncState CachedAsyncState private StatementCompletedEventHandler _statementCompletedEventHandler; - private TdsParserStateObject _stateObj; // this is the TDS session we're using. - // Volatile bool used to synchronize with cancel thread the state change of an executing // command going from pre-processing to obtaining a stateObject. The cancel synchronization // we require in the command is only from entering an Execute* API to obtaining a @@ -901,131 +854,6 @@ private void PropertyChanging() this.IsDirty = true; } - /// - public override void Prepare() - { - SqlConnection.ExecutePermission.Demand(); - - // Reset _pendingCancel upon entry into any Execute - used to synchronize state - // between entry into Execute* API and the thread obtaining the stateObject. - _pendingCancel = false; - - using (TryEventScope.Create("SqlCommand.Prepare | API | Object Id {0}", ObjectID)) - { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); - - SqlStatistics statistics = SqlStatistics.StartTimer(Statistics); - - // only prepare if batch with parameters - // MDAC BUG #'s 73776 & 72101 - if ( - IsPrepared && !IsDirty - || CommandType == CommandType.StoredProcedure - || (CommandType == CommandType.Text && GetParameterCount(_parameters) == 0) - ) - { - if (Statistics != null) - { - Statistics.SafeIncrement(ref Statistics._prepares); - } - _hiddenPrepare = false; - } - else - { - // Validate the command outside of the try\catch to avoid putting the _stateObj on error - ValidateCommand(isAsync: false); - - bool processFinallyBlock = true; - try - { - // NOTE: The state object isn't actually needed for this, but it is still here for back-compat (since it does a bunch of checks) - GetStateObject(); - - // Loop through parameters ensuring that we do not have unspecified types, sizes, scales, or precisions - if (_parameters != null) - { - int count = _parameters.Count; - for (int i = 0; i < count; ++i) - { - _parameters[i].Prepare(this); // MDAC 67063 - } - } - - InternalPrepare(); - } - // @TODO: CER Exception Handling was removed here (see GH#3581) - catch (Exception e) - { - processFinallyBlock = ADP.IsCatchableExceptionType(e); - throw; - } - finally - { - if (processFinallyBlock) - { - _hiddenPrepare = false; // The command is now officially prepared - - ReliablePutStateObject(); - } - } - } - - SqlStatistics.StopTimer(statistics); - } - } - - private void InternalPrepare() - { - if (this.IsDirty) - { - Debug.Assert(_cachedMetaData == null || !_dirty, "dirty query should not have cached metadata!"); // can have cached metadata if dirty because of parameters - // - // someone changed the command text or the parameter schema so we must unprepare the command - // - this.Unprepare(); - this.IsDirty = false; - } - Debug.Assert(_execType != EXECTYPE.PREPARED, "Invalid attempt to Prepare already Prepared command!"); - Debug.Assert(_activeConnection != null, "must have an open connection to Prepare"); - Debug.Assert(_stateObj != null, "TdsParserStateObject should not be null"); - Debug.Assert(_stateObj.Parser != null, "TdsParser class should not be null in Command.Execute!"); - Debug.Assert(_stateObj.Parser == _activeConnection.Parser, "stateobject parser not same as connection parser"); - Debug.Assert(false == _inPrepare, "Already in Prepare cycle, this.inPrepare should be false!"); - - // remember that the user wants to do a prepare but don't actually do an rpc - _execType = EXECTYPE.PREPAREPENDING; - // Note the current close count of the connection - this will tell us if the connection has been closed between calls to Prepare() and Execute - _preparedConnectionCloseCount = _activeConnection.CloseCount; - _preparedConnectionReconnectCount = _activeConnection.ReconnectCount; - - if (Statistics != null) - { - Statistics.SafeIncrement(ref Statistics._prepares); - } - } - - // SqlInternalConnectionTds needs to be able to unprepare a statement - internal void Unprepare() - { - Debug.Assert(true == IsPrepared, "Invalid attempt to Unprepare a non-prepared command!"); - Debug.Assert(_activeConnection != null, "must have an open connection to UnPrepare"); - Debug.Assert(false == _inPrepare, "_inPrepare should be false!"); - _execType = EXECTYPE.PREPAREPENDING; - - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.UnPrepare | Info | Object Id {0}, Current Prepared Handle {1}", ObjectID, _prepareHandle); - - // Don't zero out the handle because we'll pass it in to sp_prepexec on the next prepare - // Unless the close count isn't the same as when we last prepared - if ((_activeConnection.CloseCount != _preparedConnectionCloseCount) || (_activeConnection.ReconnectCount != _preparedConnectionReconnectCount)) - { - // reset our handle - _prepareHandle = -1; - } - - _cachedMetaData = null; - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.UnPrepare | Info | Object Id {0}, Command unprepared.", ObjectID); - } - // Cancel is supposed to be multi-thread safe. // It doesn't make sense to verify the connection exists or that it is open during cancel // because immediately after checkin the connection can be closed or removed via another thread. @@ -6592,48 +6420,6 @@ internal void OnConnectionClosed() } } - internal TdsParserStateObject StateObject - { - get - { - return _stateObj; - } - } - - private bool IsPrepared - { - get { return (_execType != EXECTYPE.UNPREPARED); } - } - - private bool IsUserPrepared - { - get { return IsPrepared && !_hiddenPrepare && !IsDirty; } - } - - internal bool IsDirty - { - get - { - // only dirty if prepared - var activeConnection = _activeConnection; - return (IsPrepared && - (_dirty || - ((_parameters != null) && (_parameters.IsDirty)) || - ((activeConnection != null) && ((activeConnection.CloseCount != _preparedConnectionCloseCount) || (activeConnection.ReconnectCount != _preparedConnectionReconnectCount))))); - } - set - { - // only mark the command as dirty if it is already prepared - // but always clear the value if it we are clearing the dirty flag - _dirty = value ? IsPrepared : false; - if (_parameters != null) - { - _parameters.IsDirty = _dirty; - } - _cachedMetaData = null; - } - } - /// /// Get or add to the number of records affected by SpDescribeParameterEncryption. /// The below line is used only for debug asserts and not exposed publicly or impacts functionality otherwise. diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientEventSource.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientEventSource.cs index 4988a4f4da..6700f7d1b3 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientEventSource.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientEventSource.cs @@ -841,6 +841,16 @@ internal void TryAdvancedTraceErrorEvent(string message, T0 #endregion #region Correlation Trace + + [NonEvent] + internal void TryCorrelationTraceEvent(string message) + { + if (Log.IsCorrelationEnabled()) + { + CorrelationTrace(message); + } + } + [NonEvent] internal void TryCorrelationTraceEvent(string message, T0 args0) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs new file mode 100644 index 0000000000..67865bb155 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -0,0 +1,311 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Data; +using System.Data.Common; +using System.Diagnostics; +using System.Threading; +using Microsoft.Data.Common; + +namespace Microsoft.Data.SqlClient +{ + /// + [DefaultEvent("RecordsAffected")] + [DesignerCategory("")] + [ToolboxItem(true)] + public sealed partial class SqlCommand : DbCommand, ICloneable + { + #region Constants + + private static readonly object s_cachedInvalidPrepareHandle = (object)-1; + + #endregion + + #region Fields + + /// + /// Number of instances of SqlCommand that have been created. Used to generate ObjectId + /// + private static int _objectTypeCount = 0; + + /// + /// Connection that will be used to process the current instance. + /// + private SqlConnection _activeConnection; + + /// + /// Text to execute when executing the command. + /// + private string _commandText; + + /// + /// Type of the command to execute. + /// + private CommandType _commandType; + + /// + /// Current state of preparation of the command. + /// By default, assume the user is not sharing a connection so the command has not been prepared. + /// + private EXECTYPE _execType = EXECTYPE.UNPREPARED; + + /// + /// True if the user changes the command text or number of parameters after the command has + /// already prepared. + /// + // @TODO: Consider renaming "_IsUserDirty" + private bool _dirty = false; + + /// + /// On 8.0 and above the Prepared state cannot be left. Once a command is prepared it will + /// always be prepared. A change in parameters, command text, etc (IsDirty) automatically + /// causes a hidden prepare. + /// + private bool _hiddenPrepare = false; + + /// + /// _inPrepare will be set immediately before the actual prepare is done. The OnReturnValue + /// function will test this flag to determine whether the returned value is a + /// _prepareHandle or something else. + /// + // @TODO: Make auto-property + private bool _inPrepare = false; + + /// + /// The handle of a prepared command. Apparently there can be multiple prepared commands at + /// a time - a feature that we do not support yet. this is an int which is used in the + /// object typed SqlParameter.Value field, avoid repeated boxing by storing in a box. + /// + private object _prepareHandle = s_cachedInvalidPrepareHandle; // this is an int which is used in the object typed SqlParameter.Value field, avoid repeated boxing by storing in a box + + /// + /// TDS session the current instance is using. + /// + private TdsParserStateObject _stateObj; + + #endregion + + #region Enums + + // @TODO: Rename to match naming conventions + private enum EXECTYPE + { + /// + /// Execute unprepared commands, all server versions (results in sp_execsql call) + /// + UNPREPARED, + + /// + /// Prepare and execute command, 8.0 and above only (results in sp_prepexec call) + /// + PREPAREPENDING, + + /// + /// execute prepared commands, all server versions (results in sp_exec call) + /// + PREPARED, + } + + #endregion + + #region Properties + + internal bool InPrepare => _inPrepare; + + // @TODO: Rename to match conventions. + internal int ObjectID { get; } = Interlocked.Increment(ref _objectTypeCount); + + private bool IsDirty + { + get + { + // @TODO: Factor out closeCount/reconnectCount checks to properties and clean up. + // To wit: closeCount checks whether the connection has been closed after preparation, + // reconnectCount, the same only with reconnections. + + // only dirty if prepared + // @TODO: we probably do not need to store this as a temp variable. + var activeConnection = _activeConnection; + return IsPrepared && + (_dirty || + (_parameters != null && _parameters.IsDirty) || + (activeConnection != null && (activeConnection.CloseCount != _preparedConnectionCloseCount || activeConnection.ReconnectCount != _preparedConnectionReconnectCount))); + } + set + { + // @TODO: Consider reworking to do this in a helper method, since setting, sets to the + // _dirty, but that's not the only consideration when determining dirtiness. + + // only mark the command as dirty if it is already prepared + // but always clear the value if we are clearing the dirty flag + _dirty = value ? IsPrepared : false; + if (_parameters != null) + { + _parameters.IsDirty = _dirty; + } + _cachedMetaData = null; + } + } + + private bool IsPrepared => _execType is not EXECTYPE.UNPREPARED; + + // @TODO: IsPrepared is part of IsDirty - this is confusing. + private bool IsUserPrepared => IsPrepared && !_hiddenPrepare && !IsDirty; + + private bool IsStoredProcedure => CommandType is CommandType.StoredProcedure; + + private bool IsSimpleTextQuery => CommandType is CommandType.Text && + (_parameters is null || _parameters.Count == 0); + + #endregion + + #region Public/Internal Methods + + /// + public override void Prepare() + { + #if NETFRAMEWORK + SqlConnection.ExecutePermission.Demand(); + #endif + + using var eventScope = TryEventScope.Create($"SqlCommand.Prepare | API | Object Id {ObjectID}"); + SqlClientEventSource.Log.TryCorrelationTraceEvent( + "SqlCommand.Prepare | API | Correlation | " + + $"Object Id {ObjectID}, " + + $"ActivityID {ActivityCorrelator.Current}, " + + $"Client Connection Id {_activeConnection?.ClientConnectionId}"); + + // Reset _pendingCancel upon entry into any Execute - used to synchronize state + // between entry into Execute* API and the thread obtaining the stateObject. + _pendingCancel = false; + + SqlStatistics statistics = null; + try + { + statistics = SqlStatistics.StartTimer(Statistics); + + // Only prepare batch that has parameters + // @TODO: IsPrepared is part of IsDirty - this is confusing. + if ((IsPrepared && !IsDirty) || IsStoredProcedure || IsSimpleTextQuery) + { + // @TODO: Make a simpler SafeIncrementPrepares + Statistics?.SafeIncrement(ref Statistics._prepares); + _hiddenPrepare = false; + } + else + { + // @TODO: Makethis whole else block "Prepare Internal" + + // Validate the command outside the try\catch to avoid putting the _stateObj on error + ValidateCommand(isAsync: false); + + bool processFinallyBlock = true; + try + { + // NOTE: The state object isn't actually needed for this, but it is still here for back-compat (since it does a bunch of checks) + GetStateObject(); + + // Loop through parameters ensuring that we do not have unspecified types, sizes, scales, or precisions + if (_parameters != null) + { + int count = _parameters.Count; + for (int i = 0; i < count; ++i) + { + _parameters[i].Prepare(this); + } + } + + InternalPrepare(); + } + // @TODO: CER Exception Handling was removed here (see GH#3581) + catch (Exception e) + { + processFinallyBlock = ADP.IsCatchableExceptionType(e); + throw; + } + finally + { + if (processFinallyBlock) + { + // The command is now officially prepared + _hiddenPrepare = false; + ReliablePutStateObject(); + } + } + } + } + finally + { + SqlStatistics.StopTimer(statistics); + } + } + + #endregion + + #region Private Methods + + // @TODO: Rename to PrepareInternal + private void InternalPrepare() + { + if (IsDirty) + { + Debug.Assert(_cachedMetaData == null || !_dirty, "dirty query should not have cached metadata!"); // can have cached metadata if dirty because of parameters + + // Someone changed the command text or the parameter schema so we must unprepare the command + Unprepare(); + IsDirty = false; + } + + Debug.Assert(_execType is not EXECTYPE.PREPARED, "Invalid attempt to Prepare already Prepared command!"); + Debug.Assert(_activeConnection is not null, "must have an open connection to Prepare"); + Debug.Assert(_stateObj is not null, "TdsParserStateObject should not be null"); + Debug.Assert(_stateObj.Parser is not null, "TdsParser class should not be null in Command.Execute!"); + Debug.Assert(_stateObj.Parser == _activeConnection.Parser, "stateobject parser not same as connection parser"); + Debug.Assert(!_inPrepare, "Already in Prepare cycle, this.inPrepare should be false!"); + + // Remember that the user wants to prepare but don't actually do an RPC + _execType = EXECTYPE.PREPAREPENDING; + + // Note the current close count of the connection - this will tell us if the + // connection has been closed between calls to Prepare() and Execute + _preparedConnectionCloseCount = _activeConnection.CloseCount; + _preparedConnectionReconnectCount = _activeConnection.ReconnectCount; + + Statistics?.SafeIncrement(ref Statistics._prepares); + } + + private void Unprepare() + { + Debug.Assert(IsPrepared, "Invalid attempt to Unprepare a non-prepared command!"); + Debug.Assert(_activeConnection is not null, "must have an open connection to UnPrepare"); + Debug.Assert(!_inPrepare, "_inPrepare should be false!"); + + SqlClientEventSource.Log.TryTraceEvent( + "SqlCommand.UnPrepare | Info | " + + $"Object Id {ObjectID}, " + + $"Current Prepared Handle {_prepareHandle}"); + + _execType = EXECTYPE.PREPAREPENDING; + + // Don't zero out the handle because we'll pass it in to sp_prepexec on the next prepare + // Unless the close count isn't the same as when we last prepared + if (_activeConnection.CloseCount != _preparedConnectionCloseCount || + _activeConnection.ReconnectCount != _preparedConnectionReconnectCount) + { + // Reset our handle + _prepareHandle = s_cachedInvalidPrepareHandle; + } + + _cachedMetaData = null; + + SqlClientEventSource.Log.TryTraceEvent( + $"SqlCommand.UnPrepare | Info | " + + $"Object Id {ObjectID}, Command unprepared."); + } + + #endregion + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.stub.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.stub.cs deleted file mode 100644 index bb7e537cf8..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.stub.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -// @TODO: This is only a stub class for removing clearing errors while merging other files. - -namespace Microsoft.Data.SqlClient -{ - public class SqlCommand - { - public SqlConnection Connection { get; set; } - - internal SqlStatistics Statistics { get; set; } - } -} diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlCommandTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlCommandTest.cs index c5e3b000dc..80ceba25af 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlCommandTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlCommandTest.cs @@ -411,6 +411,8 @@ public void ExecuteScalar_Connection_Null() Assert.StartsWith("ExecuteScalar:", ex.Message); } + #region Prepare() + [Fact] public void Prepare_Connection_Null() { @@ -427,51 +429,92 @@ public void Prepare_Connection_Null() } [Fact] - public void Prepare_Connection_Closed() + public void Prepare_ConnectionClosed_TextWithoutParams() { - string connectionString = "Initial Catalog=a;Server=b;User ID=c;" - + "Password=d"; - SqlConnection cn = new SqlConnection(connectionString); - - SqlCommand cmd; - - // Text, without parameters - cmd = new SqlCommand("select count(*) from whatever", cn); - cmd.Prepare(); - - // Text, with parameters - cmd = new SqlCommand("select count(*) from whatever", cn); - cmd.Parameters.Add("@TestPar1", SqlDbType.Int); - - InvalidOperationException ex = Assert.Throws(() => cmd.Prepare()); - // Prepare requires an open and available - // Connection. The connection's current state - // is Closed - Assert.Null(ex.InnerException); - Assert.NotNull(ex.Message); - Assert.True(ex.Message.IndexOf("Prepare", StringComparison.Ordinal) != -1); + // Arrange + using SqlConnection connection = GetNonConnectingConnection(); + using SqlCommand command = new SqlCommand(); + command.Connection = connection; + command.CommandType = CommandType.Text; + command.CommandText = COMMAND_TEXT; + + // Act / Assert + command.Prepare(); + Assert.Equal(ConnectionState.Closed, connection.State); + } - // Text, parameters cleared - cmd = new SqlCommand("select count(*) from whatever", cn); - cmd.Parameters.Add("@TestPar1", SqlDbType.Int); - cmd.Parameters.Clear(); - cmd.Prepare(); + [Fact] + public void Prepare_ConnectionClosed_TextWithClearedParams() + { + // Arrange + using SqlConnection connection = GetNonConnectingConnection(); + using SqlCommand command = new SqlCommand(); + command.Connection = connection; + command.CommandType = CommandType.Text; + command.CommandText = COMMAND_TEXT; + command.Parameters.Add("@TestPar1", SqlDbType.Int); + command.Parameters.Clear(); + + // Act / Assert + command.Prepare(); + Assert.Equal(ConnectionState.Closed, connection.State); + } - // StoredProcedure, without parameters - cmd = new SqlCommand("FindCustomer", cn); - cmd.CommandType = CommandType.StoredProcedure; - cmd.Prepare(); + [Fact] + public void Prepare_ConnectionClosed_TextWithParams() + { + // Arrange + using SqlConnection connection = GetNonConnectingConnection(); + using SqlCommand command = new SqlCommand(); + command.Connection = connection; + command.CommandType = CommandType.Text; + command.CommandText = COMMAND_TEXT; + command.Parameters.Add("@TestPar1", SqlDbType.Int); + + // Act + Action action = () => command.Prepare(); + + // Assert + var exception = Assert.Throws(action); + Assert.Null(exception.InnerException); + Assert.NotNull(exception.Message); + Assert.Contains("Prepare", exception.Message); + + Assert.Equal(ConnectionState.Closed, connection.State); + } - // StoredProcedure, with parameters - cmd = new SqlCommand("FindCustomer", cn); - cmd.CommandType = CommandType.StoredProcedure; - cmd.Parameters.Add("@TestPar1", SqlDbType.Int); - cmd.Prepare(); + [Fact] + public void Prepare_ConnectionClosed_SprocWithoutParams() + { + // Arrange + using SqlConnection connection = GetNonConnectingConnection(); + using SqlCommand command = new SqlCommand(); + command.Connection = connection; + command.CommandType = CommandType.StoredProcedure; + command.CommandText = "FindCustomer"; + + // Act / Assert + command.Prepare(); + Assert.Equal(ConnectionState.Closed, connection.State); + } - // ensure connection was not implictly opened - Assert.Equal(ConnectionState.Closed, cn.State); + [Fact] + public void Prepare_ConnectionClosed_SprocWithParams() + { + // Arrange + using SqlConnection connection = GetNonConnectingConnection(); + using SqlCommand command = new SqlCommand(); + command.Connection = connection; + command.CommandType = CommandType.StoredProcedure; + command.CommandText = "FindCustomer"; + + // Act / Assert + command.Prepare(); + Assert.Equal(ConnectionState.Closed, connection.State); } + #endregion + [Fact] public void ResetCommandTimeout() { @@ -518,5 +561,8 @@ public void ParameterCollectionTest() cmd.Parameters.Remove(cmd.Parameters[0]); } } + + private static SqlConnection GetNonConnectingConnection() => + new SqlConnection("Initial Catalog=a;Server=b;User ID=c;Password=d"); } }