File: System\Data\SqlClient\SqlInternalConnection.cs
Project: ndp\fx\src\data\System.Data.csproj (System.Data)
//------------------------------------------------------------------------------
// <copyright file="SqlInternalConnection.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// <owner current="true" primary="true">Microsoft</owner>
// <owner current="true" primary="false">Microsoft</owner>
//------------------------------------------------------------------------------
 
namespace System.Data.SqlClient
{
    using System;
    using System.Collections.Generic;
    using System.Data;
    using System.Data.Common;
    using System.Data.ProviderBase;
    using System.Diagnostics;
    using System.Globalization;
    using System.Reflection;
    using System.Runtime.CompilerServices;
    using System.Runtime.ConstrainedExecution;
    using System.Runtime.InteropServices;
    using System.Security;
    using System.Security.Permissions;
    using System.Text;
    using System.Threading;
    using SysTx = System.Transactions;
 
    abstract internal class SqlInternalConnection : DbConnectionInternal {
        private readonly SqlConnectionString _connectionOptions;
        private bool                         _isEnlistedInTransaction; // is the server-side connection enlisted? true while we're enlisted, reset only after we send a null...
        private byte[]                       _promotedDTCToken;        // token returned by the server when we promote transaction
        private byte[]                       _whereAbouts;             // cache the whereabouts (DTC Address) for exporting
        
        private bool                         _isGlobalTransaction = false; // Whether this is a Global Transaction (Non-MSDTC, Azure SQL DB Transaction)
        private bool                         _isGlobalTransactionEnabledForServer = false; // Whether Global Transactions are enabled for this Azure SQL DB Server
        private static readonly Guid         _globalTransactionTMID = new Guid("1c742caf-6680-40ea-9c26-6b6846079764"); // ID of the Non-MSDTC, Azure SQL DB Transaction Manager
 
        private bool _isAzureSQLConnection = false; // If connected to Azure SQL
 
        // if connection is not open: null
        // if connection is open: currently active database
        internal string CurrentDatabase { get; set; }
 
        // if connection is not open yet, CurrentDataSource is null
        // if connection is open:
        // * for regular connections, it is set to Data Source value from connection string
        // * for connections with FailoverPartner, it is set to the FailoverPartner value from connection string if the connection was opened to it.
        internal string CurrentDataSource { get; set; }
 
        // the delegated (or promoted) transaction we're responsible for.
        internal SqlDelegatedTransaction DelegatedTransaction { get; set; }
 
        internal enum TransactionRequest {
            Begin,
            Promote,
            Commit,
            Rollback,
            IfRollback,
            Save
        };
 
        internal SqlInternalConnection(SqlConnectionString connectionOptions) : base() {
            Debug.Assert(null != connectionOptions, "null connectionOptions?");
            _connectionOptions = connectionOptions;
        }
 
        internal SqlConnection Connection {
            get {
                return (SqlConnection)Owner;
            }
        }
 
        internal SqlConnectionString ConnectionOptions {
            get {
                return _connectionOptions;
            }
        }
 
        abstract internal SqlInternalTransaction CurrentTransaction {
            get;
        }
 
        // SQLBU 415870
        //  Get the internal transaction that should be hooked to a new outer transaction
        //  during a BeginTransaction API call.  In some cases (i.e. connection is going to 
        //  be reset), CurrentTransaction should not be hooked up this way.
        virtual internal SqlInternalTransaction AvailableInternalTransaction {
            get {
                return CurrentTransaction;
            }
        }
 
        abstract internal SqlInternalTransaction PendingTransaction {
            get;
        }
 
        override protected internal bool IsNonPoolableTransactionRoot {
            get {
                return IsTransactionRoot;  // default behavior is that root transactions are NOT poolable.  Subclasses may override.
            }
        }
 
        override internal bool IsTransactionRoot {
            get {
                var delegatedTransaction = DelegatedTransaction;
                return ((null != delegatedTransaction) && (delegatedTransaction.IsActive));
            }
        }
 
        internal bool HasLocalTransaction {
            get {
                SqlInternalTransaction currentTransaction = CurrentTransaction;
                bool result = (null != currentTransaction && currentTransaction.IsLocal);
                return result;
            }
        }
 
        internal bool HasLocalTransactionFromAPI {
            get {
                SqlInternalTransaction currentTransaction = CurrentTransaction;
                bool result = (null != currentTransaction && currentTransaction.HasParentTransaction);
                return result;
            }
        }
 
        internal bool IsEnlistedInTransaction {
            get {
                return _isEnlistedInTransaction;
            }
        }
 
        abstract internal bool IsLockedForBulkCopy {
            get;            
        }
 
        abstract internal bool IsShiloh {
            get;
        }
 
        abstract internal bool IsYukonOrNewer {
            get;
        }
 
        abstract internal bool IsKatmaiOrNewer {
            get;
        }
 
        internal byte[] PromotedDTCToken {
            get {
                return _promotedDTCToken;
            }
            set {
                _promotedDTCToken = value;
            }
        }
 
        internal bool IsGlobalTransaction {
            get {
                return _isGlobalTransaction;
            }
            set {
                _isGlobalTransaction = value;
            }
        }
 
        internal bool IsGlobalTransactionsEnabledForServer {
            get {
                return _isGlobalTransactionEnabledForServer;
            }
            set {
                _isGlobalTransactionEnabledForServer = value;
            }
        }
 
        internal bool IsAzureSQLConnection
        {
            get
            {
                return _isAzureSQLConnection;
            }
            set
            {
                _isAzureSQLConnection = value;
            }
        }
 
        override public DbTransaction BeginTransaction(IsolationLevel iso) {
            return BeginSqlTransaction(iso, null, false);
        }
 
        virtual internal SqlTransaction BeginSqlTransaction(IsolationLevel iso, string transactionName, bool shouldReconnect) {
            SqlStatistics statistics = null;
            TdsParser bestEffortCleanupTarget = null;
            RuntimeHelpers.PrepareConstrainedRegions();
            try {
#if DEBUG
                TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
 
                RuntimeHelpers.PrepareConstrainedRegions();
                try {
                    tdsReliabilitySection.Start();
#else
                {
#endif //DEBUG
                    bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(Connection);
                    statistics = SqlStatistics.StartTimer(Connection.Statistics);
 
                    SqlConnection.ExecutePermission.Demand(); // MDAC 81476
 
                    ValidateConnectionForExecute(null);
 
                    if (HasLocalTransactionFromAPI)
                        throw ADP.ParallelTransactionsNotSupported(Connection);
 
                    if (iso == IsolationLevel.Unspecified) {
                        iso = IsolationLevel.ReadCommitted; // Default to ReadCommitted if unspecified.
                    }
 
                    SqlTransaction transaction = new SqlTransaction(this, Connection, iso, AvailableInternalTransaction);
                    transaction.InternalTransaction.RestoreBrokenConnection = shouldReconnect;
                    ExecuteTransaction(TransactionRequest.Begin, transactionName, iso, transaction.InternalTransaction, false);
                    transaction.InternalTransaction.RestoreBrokenConnection = false;
                    return transaction;
                }
#if DEBUG
                finally {
                    tdsReliabilitySection.Stop();
                }
#endif //DEBUG
            }
            catch (System.OutOfMemoryException e) {
                Connection.Abort(e);
                throw;
            }
            catch (System.StackOverflowException e) {
                Connection.Abort(e);
                throw;
            }
            catch (System.Threading.ThreadAbortException e) {
                Connection.Abort(e);
                SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget);
                throw;
            }
            finally {
                SqlStatistics.StopTimer(statistics);
            }
        }
 
        override public void ChangeDatabase(string database) {
            SqlConnection.ExecutePermission.Demand(); // MDAC 80961
 
            if (ADP.IsEmpty(database)) {
                throw ADP.EmptyDatabaseName();
            }
 
            ValidateConnectionForExecute(null); // 
 
            ChangeDatabaseInternal(database);  // do the real work...
        }
 
        abstract protected void ChangeDatabaseInternal(string database);
        
        override protected void CleanupTransactionOnCompletion(SysTx.Transaction transaction) {
            // Note: unlocked, potentially multi-threaded code, so pull delegate to local to 
            //  ensure it doesn't change between test and call.
            SqlDelegatedTransaction delegatedTransaction = DelegatedTransaction;
            if (null != delegatedTransaction) {
                delegatedTransaction.TransactionEnded(transaction);
            }
        }
 
        override protected DbReferenceCollection CreateReferenceCollection() {
            return new SqlReferenceCollection();
        }
 
        override protected void Deactivate() {
            if (Bid.AdvancedOn) {
                Bid.Trace("<sc.SqlInternalConnection.Deactivate|ADV> %d# deactivating\n", base.ObjectID);
            }
            TdsParser bestEffortCleanupTarget = null;
            RuntimeHelpers.PrepareConstrainedRegions();
            try {
 
#if DEBUG
                TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
 
                RuntimeHelpers.PrepareConstrainedRegions();
                try {
                    tdsReliabilitySection.Start();
#else
                {
#endif //DEBUG
                    bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(Connection);
                    SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection;
                    if (null != referenceCollection) {
                        referenceCollection.Deactivate();
                    }
 
                    // Invoke subclass-specific deactivation logic
                    InternalDeactivate();
                }
#if DEBUG
                finally {
                    tdsReliabilitySection.Stop();
                }
#endif //DEBUG
            }
            catch (System.OutOfMemoryException) {
                DoomThisConnection();
                throw;
            }
            catch (System.StackOverflowException) {
                DoomThisConnection();
                throw;
            }
            catch (System.Threading.ThreadAbortException) {
                DoomThisConnection();
                SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget);
                throw;
            }
            catch (Exception e) {
                // 
                if (!ADP.IsCatchableExceptionType(e)) {
                    throw;
                }
 
                // if an exception occurred, the inner connection will be
                // marked as unusable and destroyed upon returning to the
                // pool
                DoomThisConnection();
 
                ADP.TraceExceptionWithoutRethrow(e);
            }
        }
       
        abstract internal void DisconnectTransaction(SqlInternalTransaction internalTransaction);
 
        override public void Dispose() {
            _whereAbouts = null;
            base.Dispose();
        }
 
        protected void Enlist(SysTx.Transaction tx) {
            // This method should not be called while the connection has a 
            // reference to an active delegated transaction.
            // Manual enlistment via SqlConnection.EnlistTransaction
            // should catch this case and throw an exception.
            //
            // Automatic enlistment isn't possible because 
            // Sys.Tx keeps the connection alive until the transaction is completed.
            Debug.Assert (!IsNonPoolableTransactionRoot, "cannot defect an active delegated transaction!");  // potential race condition, but it's an assert
 
            if (null == tx) {
                if (IsEnlistedInTransaction)
                {
                    EnlistNull();
                }
                else 
                {
                    // When IsEnlistedInTransaction is false, it means we are in one of two states:
                    // 1. EnlistTransaction is null, so the connection is truly not enlisted in a transaction, or
                    // 2. Connection is enlisted in a SqlDelegatedTransaction.
                    //
                    // For #2, we have to consider whether or not the delegated transaction is active.
                    // If it is not active, we allow the enlistment in the NULL transaction.
                    //
                    // If it is active, technically this is an error.
                    // However, no exception is thrown as this was the precedent (and this case is silently ignored, no error, but no enlistment either).
                    // There are two mitigations for this:
                    // 1. SqlConnection.EnlistTransaction checks that the enlisted transaction has completed before allowing a different enlistment.
                    // 2. For debug builds, the assert at the beginning of this method checks for an enlistment in an active delegated transaction.
                    SysTx.Transaction enlistedTransaction = EnlistedTransaction;
                    if (enlistedTransaction != null && enlistedTransaction.TransactionInformation.Status != SysTx.TransactionStatus.Active)
                    {
                        EnlistNull();
                    }
                }
            }
            // Only enlist if it's different...
            else if (!tx.Equals(EnlistedTransaction)) { // WebData 20000024 - Must use Equals, not !=
                EnlistNonNull(tx);
            }
        }
 
        private void EnlistNonNull(SysTx.Transaction tx) {
            Debug.Assert(null != tx, "null transaction?");
 
            if (Bid.AdvancedOn) {
                Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, transaction %d#.\n", base.ObjectID, tx.GetHashCode());
            }
            
            bool hasDelegatedTransaction = false;
 
            if (IsYukonOrNewer) {
                if (Bid.AdvancedOn) {
                    Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, attempting to delegate\n", base.ObjectID);
                }
 
                // Promotable transactions are only supported on Yukon
                // servers or newer.
                SqlDelegatedTransaction delegatedTransaction = new SqlDelegatedTransaction(this, tx);
                
                try {
                    // NOTE: System.Transactions claims to resolve all
                    // potential race conditions between multiple delegate
                    // requests of the same transaction to different
                    // connections in their code, such that only one
                    // attempt to delegate will succeed.
 
                    // NOTE: PromotableSinglePhaseEnlist will eventually
                    // make a round trip to the server; doing this inside
                    // a lock is not the best choice.  We presume that you
                    // aren't trying to enlist concurrently on two threads
                    // and leave it at that -- We don't claim any thread
                    // safety with regard to multiple concurrent requests
                    // to enlist the same connection in different
                    // transactions, which is good, because we don't have
                    // it anyway.
 
                    // PromotableSinglePhaseEnlist may not actually promote
                    // the transaction when it is already delegated (this is
                    // the way they resolve the race condition when two
                    // threads attempt to delegate the same Lightweight
                    // Transaction)  In that case, we can safely ignore
                    // our delegated transaction, and proceed to enlist
                    // in the promoted one.
 
                    // NOTE: Global Transactions is an Azure SQL DB only 
                    // feature where the Transaction Manager (TM) is not
                    // MS-DTC. Sys.Tx added APIs to support Non MS-DTC
                    // promoter types/TM in .NET 4.6.1. Following directions
                    // from .NETFX shiproom, to avoid a "hard-dependency"
                    // (compile time) on Sys.Tx, we use reflection to invoke
                    // the new APIs. Further, the _isGlobalTransaction flag
                    // indicates that this is an Azure SQL DB Transaction
                    // that could be promoted to a Global Transaction (it's
                    // always false for on-prem Sql Server). The Promote()
                    // call in SqlDelegatedTransaction makes sure that the
                    // right Sys.Tx.dll is loaded and that Global Transactions
                    // are actually allowed for this Azure SQL DB.
 
                    if (_isGlobalTransaction) {
                        if (SysTxForGlobalTransactions.EnlistPromotableSinglePhase == null) {
                            // This could be a local Azure SQL DB transaction. 
                            hasDelegatedTransaction = tx.EnlistPromotableSinglePhase(delegatedTransaction);
                        }
                        else {
                            hasDelegatedTransaction = (bool)SysTxForGlobalTransactions.EnlistPromotableSinglePhase.Invoke(tx, new object[] { delegatedTransaction, _globalTransactionTMID });
                        }
                    }
                    else {
                        // This is an MS-DTC distributed transaction
                        hasDelegatedTransaction = tx.EnlistPromotableSinglePhase(delegatedTransaction);
                    }
 
                    if (hasDelegatedTransaction) {
 
                        this.DelegatedTransaction = delegatedTransaction;
 
                        if (Bid.AdvancedOn) {
                            long transactionId = SqlInternalTransaction.NullTransactionId;
                            int transactionObjectID = 0; 
                            if (null != CurrentTransaction) {
                                transactionId = CurrentTransaction.TransactionId;
                                transactionObjectID = CurrentTransaction.ObjectID;
                            }
                            Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, delegated to transaction %d# with transactionId=0x%I64x\n", base.ObjectID, transactionObjectID, transactionId);
                        }
                    }
                }
                catch (SqlException e) {
                    // we do not want to eat the error if it is a fatal one
                    if (e.Class >= TdsEnums.FATAL_ERROR_CLASS) {
                        throw;
                    }
 
                    // if the parser is null or its state is not openloggedin, the connection is no longer good.
                    SqlInternalConnectionTds tdsConnection = this as SqlInternalConnectionTds;
                    if (tdsConnection != null)
                    {
                        TdsParser parser = tdsConnection.Parser;
                        if (parser == null || parser.State != TdsParserState.OpenLoggedIn)
                        {
                            throw;
                        }
                    }
 
                    ADP.TraceExceptionWithoutRethrow(e);
 
                    // In this case, SqlDelegatedTransaction.Initialize
                    // failed and we don't necessarily want to reject
                    // things -- there may have been a legitimate reason
                    // for the failure.
                }
            }
            
            if (!hasDelegatedTransaction) {
                if (Bid.AdvancedOn) {
                    Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, delegation not possible, enlisting.\n", base.ObjectID);
                }
 
                byte[] cookie = null;
 
                if (_isGlobalTransaction) {
                    if (SysTxForGlobalTransactions.GetPromotedToken == null) {
                        throw SQL.UnsupportedSysTxForGlobalTransactions();
                    }
 
                    cookie = (byte[])SysTxForGlobalTransactions.GetPromotedToken.Invoke(tx, null);
                }
                else {
                    if (null == _whereAbouts) {
                        byte[] dtcAddress = GetDTCAddress();
 
                        if (null == dtcAddress) {
                            throw SQL.CannotGetDTCAddress();
                        }
                        _whereAbouts = dtcAddress;
                    }
 
                    cookie = GetTransactionCookie(tx, _whereAbouts);
                }
 
                // send cookie to server to finish enlistment
                PropagateTransactionCookie(cookie);
                
                _isEnlistedInTransaction = true;
 
                if (Bid.AdvancedOn) {
                    long transactionId = SqlInternalTransaction.NullTransactionId;
                    int transactionObjectID = 0; 
                    if (null != CurrentTransaction) {
                        transactionId = CurrentTransaction.TransactionId;
                        transactionObjectID = CurrentTransaction.ObjectID;
                    }
                    Bid.Trace("<sc.SqlInternalConnection.EnlistNonNull|ADV> %d#, enlisted with transaction %d# with transactionId=0x%I64x\n", base.ObjectID, transactionObjectID, transactionId);
                }
            }
 
            EnlistedTransaction = tx; // Tell the base class about our enlistment
 
 
            // If we're on a Yukon or newer server, and we we delegate the 
            // transaction successfully, we will have done a begin transaction, 
            // which produces a transaction id that we should execute all requests
            // on.  The TdsParser or SmiEventSink will store this information as
            // the current transaction.
            // 
            // Likewise, propagating a transaction to a Yukon or newer server will
            // produce a transaction id that The TdsParser or SmiEventSink will 
            // store as the current transaction.
            //
            // In either case, when we're working with a Yukon or newer server 
            // we better have a current transaction by now.
 
            Debug.Assert(!IsYukonOrNewer || null != CurrentTransaction, "delegated/enlisted transaction with null current transaction?");
        }
 
        internal void EnlistNull() {
            if (Bid.AdvancedOn) {
                Bid.Trace("<sc.SqlInternalConnection.EnlistNull|ADV> %d#, unenlisting.\n", base.ObjectID);
            }
 
            // We were in a transaction, but now we are not - so send
            // message to server with empty transaction - confirmed proper
            // behavior from Sameet Agarwal
            //
            // The connection pooler maintains separate pools for enlisted
            // transactions, and only when that transaction is committed or
            // rolled back will those connections be taken from that
            // separate pool and returned to the general pool of connections
            // that are not affiliated with any transactions.  When this
            // occurs, we will have a new transaction of null and we are
            // required to send an empty transaction payload to the server.
 
            PropagateTransactionCookie(null);
 
            _isEnlistedInTransaction = false;
            EnlistedTransaction = null; // Tell the base class about our enlistment
 
            if (Bid.AdvancedOn) {
                Bid.Trace("<sc.SqlInternalConnection.EnlistNull|ADV> %d#, unenlisted.\n", base.ObjectID);
            }
 
            // The EnlistTransaction above will return an TransactionEnded event, 
            // which causes the TdsParser or SmiEventSink should to clear the
            // current transaction.
            //
            // In either case, when we're working with a Yukon or newer server 
            // we better not have a current transaction at this point.
 
            Debug.Assert(!IsYukonOrNewer || null == CurrentTransaction, "unenlisted transaction with non-null current transaction?");   // verify it!
        }
        
        override public void EnlistTransaction(SysTx.Transaction transaction) {
            SqlConnection.VerifyExecutePermission();
 
            ValidateConnectionForExecute(null);
 
            // If a connection has a local transaction outstanding and you try
            // to enlist in a DTC transaction, SQL Server will rollback the
            // local transaction and then do the enlist (7.0 and 2000).  So, if
            // the user tries to do this, throw.
            if (HasLocalTransaction) {
                throw ADP.LocalTransactionPresent();
            }
 
            if (null != transaction && transaction.Equals(EnlistedTransaction)) {
                // No-op if this is the current transaction
                return;
            }
 
            // If a connection is already enlisted in a DTC transaction and you
            // try to enlist in another one, in 7.0 the existing DTC transaction
            // would roll back and then the connection would enlist in the new
            // one. In SQL 2000 & Yukon, when you enlist in a DTC transaction
            // while the connection is already enlisted in a DTC transaction,
            // the connection simply switches enlistments.  Regardless, simply
            // enlist in the user specified distributed transaction.  This
            // behavior matches OLEDB and ODBC.
 
            TdsParser bestEffortCleanupTarget = null;
            RuntimeHelpers.PrepareConstrainedRegions();
            try {
#if DEBUG
                TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();
 
                RuntimeHelpers.PrepareConstrainedRegions();
                try {
                    tdsReliabilitySection.Start();
#else
                {
#endif //DEBUG
                    bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(Connection);
                    Enlist(transaction);
                }
#if DEBUG
                finally {
                    tdsReliabilitySection.Stop();
                }
#endif //DEBUG
            }
            catch (System.OutOfMemoryException e) {
                Connection.Abort(e);
                throw;
            }
            catch (System.StackOverflowException e) {
                Connection.Abort(e);
                throw;
            }
            catch (System.Threading.ThreadAbortException e) {
                Connection.Abort(e);
                SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget);
                throw;
            }
        }
 
        abstract internal void ExecuteTransaction(TransactionRequest transactionRequest, string name, IsolationLevel iso, SqlInternalTransaction internalTransaction, bool isDelegateControlRequest);
 
        internal SqlDataReader FindLiveReader(SqlCommand command) {
            SqlDataReader reader = null;
            SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection;
            if (null != referenceCollection) {
                reader =  referenceCollection.FindLiveReader(command);
            }
            return reader;
        }
 
        internal SqlCommand FindLiveCommand(TdsParserStateObject stateObj) {
            SqlCommand command = null;
            SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection;
            if (null != referenceCollection) {
                command =  referenceCollection.FindLiveCommand(stateObj);
            }
            return command;
        }
 
        static internal TdsParser GetBestEffortCleanupTarget(SqlConnection connection) {
            if (null != connection) {
                SqlInternalConnectionTds innerConnection = (connection.InnerConnection as SqlInternalConnectionTds);
                if (null != innerConnection) {
                    return innerConnection.Parser;
                }
            }
 
            return null;
        }
 
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        static internal void BestEffortCleanup(TdsParser target) {
            if (null != target) {
                target.BestEffortCleanup();
            }
        }
 
        abstract protected byte[] GetDTCAddress();
 
        static private byte[] GetTransactionCookie(SysTx.Transaction transaction, byte[] whereAbouts) {
            byte[] transactionCookie = null;
            if (null != transaction) {
                transactionCookie = SysTx.TransactionInterop.GetExportCookie(transaction, whereAbouts);
            }
            return transactionCookie;
        }
 
        virtual protected void InternalDeactivate() {
        }
 
        // If wrapCloseInAction is defined, then the action it defines will be run with the connection close action passed in as a parameter
        // The close action also supports being run asynchronously
        internal void OnError(SqlException exception, bool breakConnection, Action<Action> wrapCloseInAction = null) {
            if (breakConnection) {
                DoomThisConnection();
            }
 
            var connection = Connection;
            if (null != connection) {
                connection.OnError(exception, breakConnection, wrapCloseInAction);
            }
            else if (exception.Class >= TdsEnums.MIN_ERROR_CLASS) {
                // It is an error, and should be thrown.  Class of TdsEnums.MIN_ERROR_CLASS
                // or above is an error, below TdsEnums.MIN_ERROR_CLASS denotes an info message.
                throw exception;
            }
        }
 
        abstract protected void PropagateTransactionCookie(byte[] transactionCookie);
        
        abstract internal void ValidateConnectionForExecute(SqlCommand command);
    }
}