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 d1dd14f6b7..1cc249d23e 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -570,6 +570,9 @@ Microsoft\Data\SqlClient\SqlColumnEncryptionKeyStoreProvider.cs + + Microsoft\Data\SqlClient\SqlCommand.cs + Microsoft\Data\SqlClient\SqlCommandSet.cs @@ -799,7 +802,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 97% 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 f4f38dbe73..0d7dbaf00f 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; @@ -164,39 +156,18 @@ protected override void AfterCleared(SqlCommand owner) private bool _parentOperationStarted = false; 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 +198,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 +346,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 +842,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. @@ -6803,48 +6640,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 0c2fe65f93..3a0d003f80 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 @@ -870,6 +873,9 @@ Microsoft\Data\SqlClient\Utilities\BufferWriterExtensions.netfx.cs + + Microsoft\Data\SqlClient\Utilities\ConstrainedExecutionHelper.netfx.cs + Microsoft\Data\SqlClient\Utilities\ObjectPool.cs @@ -913,7 +919,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 d65a0b7522..eff93177bf 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,14 @@ 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 +194,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. @@ -385,8 +344,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 @@ -917,154 +874,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; - TdsParser bestEffortCleanupTarget = null; - RuntimeHelpers.PrepareConstrainedRegions(); - try - { - bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_activeConnection); - - // 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(); - } - catch (System.OutOfMemoryException e) - { - processFinallyBlock = false; - _activeConnection.Abort(e); - throw; - } - catch (System.StackOverflowException e) - { - processFinallyBlock = false; - _activeConnection.Abort(e); - throw; - } - catch (System.Threading.ThreadAbortException e) - { - processFinallyBlock = false; - _activeConnection.Abort(e); - - SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget); - throw; - } - 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. @@ -6876,48 +6685,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 75f87ea13c..8da01fbbd7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientEventSource.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientEventSource.cs @@ -832,6 +832,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..b3343789e6 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -0,0 +1,300 @@ +// 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; +using Microsoft.Data.SqlClient.Utilities; + +namespace Microsoft.Data.SqlClient +{ + /// + [DefaultEvent("RecordsAffected")] + [DesignerCategory("")] + [ToolboxItem(true)] + public sealed partial class SqlCommand : DbCommand, ICloneable + { + #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; + + /// + /// 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 + 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; + + #endregion + + #region Public Methods + + /// + public override void Prepare() + { + #if NETFRAMEWORK + SqlConnection.ExecutePermission.Demand(); + #endif + + using var _ = TryEventScope.Create("SqlCommand.Prepare | API | Object Id {0}", ObjectID); + SqlClientEventSource.Log.TryCorrelationTraceEvent( + "SqlCommand.Prepare | API | Correlation | " + + $"Object Id {ObjectID}, " + + $"ActivityID {ActivityCorrelator.Current}, " + + $"Client Connection Id {Connection?.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 has parameters + // @TODO: Factor out stored proc/text+parameter count to property + // @TODO: Not using parentheses is confusing here b/c it relies on order of operations knowledge + if (IsPrepared && !IsDirty || CommandType == CommandType.StoredProcedure + || (CommandType == CommandType.Text && GetParameterCount(_parameters) == 0)) + { + Statistics?.SafeIncrement(ref Statistics._prepares); + _hiddenPrepare = false; + } + else + { + // Validate the command outside the try\catch to avoid putting the _stateObj on error + ValidateCommand(isAsync: false); + + bool processFinallyBlock = true; + try + { + #if NET + PrepareInternal(); + #else + ConstrainedExecutionHelper.RunWithConnectionAbortAndCleanup(PrepareInternal, _activeConnection); + #endif + } + catch (Exception e) + { + // NOTE: This will catch any CER exceptions that were rethrown in netfx. + // they will always be uncatchable, so this is equivalent to setting + // processFinallyBlock to false inside the CER handlers. + 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 + + private void PrepareInternal() + { + // 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 is not null) + { + int count = _parameters.Count; + for (int i = 0; i < count; ++i) + { + _parameters[i].Prepare(this); + } + } + + if (IsDirty) + { + Debug.Assert(_cachedMetaData is null || !_dirty, "dirty query should not have cached metadata!"); + + // Someone changed the command text or the parameter schema so we must unprepare the command + this.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 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; + + 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/src/Microsoft/Data/SqlClient/Utilities/ConstrainedExecutionHelper.netfx.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Utilities/ConstrainedExecutionHelper.netfx.cs new file mode 100644 index 0000000000..51e8a557fa --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Utilities/ConstrainedExecutionHelper.netfx.cs @@ -0,0 +1,46 @@ +// 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. + +#if NETFRAMEWORK + +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Microsoft.Data.SqlClient.Utilities +{ + internal static class ConstrainedExecutionHelper + { + internal static void RunWithConnectionAbortAndCleanup(Action action, SqlConnection connectionToAbort) + { + TdsParser bestEffortCleanupTarget = null; + + RuntimeHelpers.PrepareConstrainedRegions(); + try + { + bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(connectionToAbort); + action(); + } + catch (OutOfMemoryException e) + { + connectionToAbort.Abort(e); + throw; + } + catch (StackOverflowException e) + { + connectionToAbort.Abort(e); + throw; + } + catch (ThreadAbortException e) + { + connectionToAbort.Abort(e); + SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget); + + throw; + } + } + } +} + +#endif