diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml index 3f1d2c4ccb..8e7e927187 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml @@ -1096,6 +1096,11 @@ GO was called while the returned Task was not completed and the connection was not opened after a call to . + + Gets the server process Id (SPID) of the active connection. + The server process Id (SPID) of the active connection. + Returns 0 if the connection is inactive on the client side. + Indicates the state of the during the most recent network operation performed on the connection. An enumeration. diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs index f53e7cebdb..ea3d80c02d 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -561,7 +561,7 @@ internal string SQLDNSCachingSupportedState if (null != innerConnection) { - result = innerConnection.IsSQLDNSCachingSupported ? "true": "false"; + result = innerConnection.IsSQLDNSCachingSupported ? "true" : "false"; } else { @@ -584,7 +584,7 @@ internal string SQLDNSCachingSupportedStateBeforeRedirect if (null != innerConnection) { - result = innerConnection.IsDNSCachingBeforeRedirectSupported ? "true": "false"; + result = innerConnection.IsDNSCachingBeforeRedirectSupported ? "true" : "false"; } else { @@ -671,6 +671,13 @@ public override string ServerVersion get => GetOpenTdsConnection().ServerVersion; } + /// + public int ServerProcessId + { + get => State.Equals(ConnectionState.Open) | State.Equals(ConnectionState.Executing) | State.Equals(ConnectionState.Fetching) ? + GetOpenTdsConnection().ServerProcessId : 0; + } + /// public override ConnectionState State { diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 68b37eb6a2..1999d4365b 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -692,6 +692,14 @@ public override string ServerVersion } } + public int ServerProcessId + { + get + { + return Parser._physicalStateObj._spid; + } + } + protected override bool UnbindOnTransactionCompletion { get diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs index 043a70669d..1d2dd44c0a 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -53,6 +53,7 @@ internal static class TdsEnums // header constants public const int HEADER_LEN = 8; public const int HEADER_LEN_FIELD_OFFSET = 2; + public const int SPID_OFFSET = 4; public const int YUKON_HEADER_LEN = 12; //Yukon headers also include a MARS session id public const int MARS_ID_OFFSET = 8; public const int HEADERTYPE_QNOTIFICATION = 1; diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs index d76d74b263..167954fbc0 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs @@ -89,6 +89,8 @@ internal enum SnapshottedStateFlags : byte /// internal int _inBytesPacket; + internal int _spid; // SPID of the current connection + // Packet state variables internal byte _outputMessageType; // tds header type internal byte _messageStatus; // tds header status @@ -1019,6 +1021,8 @@ internal bool TryProcessHeader() (int)_partialHeaderBuffer[TdsEnums.HEADER_LEN_FIELD_OFFSET + 1]) - _inputHeaderLen; _messageStatus = _partialHeaderBuffer[1]; + _spid = _partialHeaderBuffer[TdsEnums.SPID_OFFSET] << 8 | + _partialHeaderBuffer[TdsEnums.SPID_OFFSET + 1]; } else { @@ -1052,8 +1056,10 @@ internal bool TryProcessHeader() { // normal header processing... _messageStatus = _inBuff[_inBytesUsed + 1]; - _inBytesPacket = ((int)_inBuff[_inBytesUsed + TdsEnums.HEADER_LEN_FIELD_OFFSET] << 8 | - (int)_inBuff[_inBytesUsed + TdsEnums.HEADER_LEN_FIELD_OFFSET + 1]) - _inputHeaderLen; + _inBytesPacket = (_inBuff[_inBytesUsed + TdsEnums.HEADER_LEN_FIELD_OFFSET] << 8 | + _inBuff[_inBytesUsed + TdsEnums.HEADER_LEN_FIELD_OFFSET + 1]) - _inputHeaderLen; + _spid = _inBuff[_inBytesUsed + TdsEnums.SPID_OFFSET] << 8 | + _inBuff[_inBytesUsed + TdsEnums.SPID_OFFSET + 1]; _inBytesUsed += _inputHeaderLen; AssertValidState(); @@ -3481,35 +3487,35 @@ internal void SendAttention(bool mustTakeWriteLock = false) if (!_skipSendAttention) { #endif - // Take lock and send attention - bool releaseLock = false; - if ((mustTakeWriteLock) && (!_parser.Connection.ThreadHasParserLockForClose)) + // Take lock and send attention + bool releaseLock = false; + if ((mustTakeWriteLock) && (!_parser.Connection.ThreadHasParserLockForClose)) + { + releaseLock = true; + _parser.Connection._parserLock.Wait(canReleaseFromAnyThread: false); + _parser.Connection.ThreadHasParserLockForClose = true; + } + try + { + // Check again (just in case the connection was closed while we were waiting) + if (_parser.State == TdsParserState.Closed || _parser.State == TdsParserState.Broken) { - releaseLock = true; - _parser.Connection._parserLock.Wait(canReleaseFromAnyThread: false); - _parser.Connection.ThreadHasParserLockForClose = true; + return; } - try - { - // Check again (just in case the connection was closed while we were waiting) - if (_parser.State == TdsParserState.Closed || _parser.State == TdsParserState.Broken) - { - return; - } - uint sniError; - _parser._asyncWrite = false; // stop async write - SNIWritePacket(attnPacket, out sniError, canAccumulate: false, callerHasConnectionLock: false); - SqlClientEventSource.Log.TraceEvent(" Send Attention ASync.", "Info"); - } - finally + uint sniError; + _parser._asyncWrite = false; // stop async write + SNIWritePacket(attnPacket, out sniError, canAccumulate: false, callerHasConnectionLock: false); + SqlClientEventSource.Log.TraceEvent(" Send Attention ASync.", "Info"); + } + finally + { + if (releaseLock) { - if (releaseLock) - { - _parser.Connection.ThreadHasParserLockForClose = false; - _parser.Connection._parserLock.Release(); - } + _parser.Connection.ThreadHasParserLockForClose = false; + _parser.Connection._parserLock.Release(); } + } #if DEBUG } #endif 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 f3b68d3da1..6e247cc897 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -128,7 +128,7 @@ Microsoft\Data\SqlClient\Server\ExtendedClrTypeCode.cs - + Microsoft\Data\SqlClient\Server\IBinarySerialize.cs diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs index d48d907e1c..f023b3dacb 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -738,7 +738,7 @@ internal string SQLDNSCachingSupportedState if (null != innerConnection) { - result = innerConnection.IsSQLDNSCachingSupported ? "true": "false"; + result = innerConnection.IsSQLDNSCachingSupported ? "true" : "false"; } else { @@ -762,7 +762,7 @@ internal string SQLDNSCachingSupportedStateBeforeRedirect if (null != innerConnection) { - result = innerConnection.IsDNSCachingBeforeRedirectSupported ? "true": "false"; + result = innerConnection.IsDNSCachingBeforeRedirectSupported ? "true" : "false"; } else { @@ -878,6 +878,18 @@ override public string ServerVersion } } + /// + [ + Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), + ResDescription(StringsHelper.ResourceNames.SqlConnection_ServerProcessId), + ] + public int ServerProcessId + { + get => State.Equals(ConnectionState.Open) | State.Equals(ConnectionState.Executing) | State.Equals(ConnectionState.Fetching) ? + GetOpenTdsConnection().ServerProcessId : 0; + } + /// [ Browsable(false), diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 100b75439f..3d033f5a60 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -803,6 +803,13 @@ override public string ServerVersion (short)_loginAck.minorVersion, _loginAck.buildNum)); } } + public int ServerProcessId + { + get + { + return Parser._physicalStateObj._spid; + } + } /// /// Get boolean that specifies whether an enlisted transaction can be unbound from diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs index a444991ddb..cf8df1524b 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -67,6 +67,7 @@ internal static class TdsEnums // header constants public const int HEADER_LEN = 8; public const int HEADER_LEN_FIELD_OFFSET = 2; + public const int SPID_OFFSET = 4; public const int YUKON_HEADER_LEN = 12; //Yukon headers also include a MARS session id public const int MARS_ID_OFFSET = 8; public const int HEADERTYPE_QNOTIFICATION = 1; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs index 09305c3cf4..62cc1b7be3 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs @@ -71,6 +71,8 @@ internal int ObjectID internal int _inBytesUsed = 0; // number of bytes used in internal read buffer internal int _inBytesRead = 0; // number of bytes read into internal read buffer internal int _inBytesPacket = 0; // number of bytes left in packet + + internal int _spid; // SPID of the current connection // Packet state variables internal byte _outputMessageType = 0; // tds header type @@ -1131,6 +1133,8 @@ internal bool TryProcessHeader() (int)_partialHeaderBuffer[TdsEnums.HEADER_LEN_FIELD_OFFSET + 1]) - _inputHeaderLen; _messageStatus = _partialHeaderBuffer[1]; + _spid = _partialHeaderBuffer[TdsEnums.SPID_OFFSET] << 8 | + _partialHeaderBuffer[TdsEnums.SPID_OFFSET + 1]; } else { @@ -1166,8 +1170,10 @@ internal bool TryProcessHeader() { // normal header processing... _messageStatus = _inBuff[_inBytesUsed + 1]; - _inBytesPacket = ((int)_inBuff[_inBytesUsed + TdsEnums.HEADER_LEN_FIELD_OFFSET] << 8 | - (int)_inBuff[_inBytesUsed + TdsEnums.HEADER_LEN_FIELD_OFFSET + 1]) - _inputHeaderLen; + _inBytesPacket = (_inBuff[_inBytesUsed + TdsEnums.HEADER_LEN_FIELD_OFFSET] << 8 | + _inBuff[_inBytesUsed + TdsEnums.HEADER_LEN_FIELD_OFFSET + 1]) - _inputHeaderLen; + _spid = _inBuff[_inBytesUsed + TdsEnums.SPID_OFFSET] << 8 | + _inBuff[_inBytesUsed + TdsEnums.SPID_OFFSET + 1]; _inBytesUsed += _inputHeaderLen; AssertValidState(); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs index a4ca42aeb4..b6b1a532df 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs @@ -8092,7 +8092,7 @@ internal class Strings { } /// - /// Looks up a localized string similar to Security Warning: The negotiated '{0}' is an insecured protocol and is supported for backward compatibility only. The recommended protocol is TLS 1.2 and later.. + /// Looks up a localized string similar to Security Warning: The negotiated {0} is an insecure protocol and is supported for backward compatibility only. The recommended protocol version is TLS 1.2 and later.. /// internal static string SEC_ProtocolWarning { get { @@ -10539,6 +10539,15 @@ internal class Strings { } } + /// + /// Looks up a localized string similar to Server Process Id (SPID) of the active connection.. + /// + internal static string SqlConnection_ServerProcessId { + get { + return ResourceManager.GetString("SqlConnection_ServerProcessId", resourceCulture); + } + } + /// /// Looks up a localized string similar to Version of the SQL Server accessed by the SqlConnection.. /// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx index 3cc34302a4..1a90bdf991 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx @@ -4560,4 +4560,7 @@ Cannot set the Credential property if 'Authentication=Active Directory Interactive' has been specified in the connection string. + + Server Process Id (SPID) of the active connection. + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs index f4a1d37fe4..ba9777a432 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs @@ -27,7 +27,7 @@ public static class ConnectivityTest private static readonly string s_dropDatabaseCmd = $"DROP DATABASE {s_databaseName}"; [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] - public static void EnvironmentHostNameTest() + public static void EnvironmentHostNameSPIDTest() { SqlConnectionStringBuilder builder = (new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString) { Pooling = true }); builder.ApplicationName = "HostNameTest"; @@ -35,6 +35,7 @@ public static void EnvironmentHostNameTest() using (SqlConnection sqlConnection = new SqlConnection(builder.ConnectionString)) { sqlConnection.Open(); + int sqlClientSPID = sqlConnection.ServerProcessId; int sessionSpid; using (SqlCommand cmd = new SqlCommand("SELECT @@SPID", sqlConnection)) @@ -43,6 +44,11 @@ public static void EnvironmentHostNameTest() reader.Read(); sessionSpid = reader.GetInt16(0); } + // Confirm Server process id is same as result of SELECT @@SPID + Assert.Equal(sessionSpid, sqlClientSPID); + + // Confirm once again SPID on SqlConnection directly + Assert.Equal(sessionSpid, sqlConnection.ServerProcessId); using (SqlCommand command = new SqlCommand("sp_who2", sqlConnection)) using (SqlDataReader reader = command.ExecuteReader()) @@ -55,7 +61,7 @@ public static void EnvironmentHostNameTest() int spidOrdinal = reader.GetOrdinal(COL_SPID); string spid = reader.GetString(spidOrdinal); - if (programName != null && programName.Trim().Equals(builder.ApplicationName) && Int16.Parse(spid) == sessionSpid) + if (programName != null && programName.Trim().Equals(builder.ApplicationName) && short.Parse(spid) == sessionSpid) { // Get the hostname int hostnameOrdinal = reader.GetOrdinal(COL_HOSTNAME); @@ -67,6 +73,8 @@ public static void EnvironmentHostNameTest() } } } + // Confirm Server Process Id stays the same after query execution + Assert.Equal(sessionSpid, sqlConnection.ServerProcessId); } Assert.True(false, "No non-empty hostname found for the application"); } @@ -106,11 +114,11 @@ public static void ConnectionTimeoutTestWithThread() } [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] - public static void ProcessIdTest() + public static void LocalProcessIdTest() { SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString); string sqlProviderName = builder.ApplicationName; - string sqlProviderProcessID = System.Diagnostics.Process.GetCurrentProcess().Id.ToString(); + string sqlProviderProcessID = Process.GetCurrentProcess().Id.ToString(); using (SqlConnection sqlConnection = new SqlConnection(builder.ConnectionString)) { @@ -228,7 +236,7 @@ public static void ConnectionKilledTest() } [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] - public static void ConnectionResiliencyTest() + public static void ConnectionResiliencySPIDTest() { SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString); builder.ConnectRetryCount = 0; @@ -256,23 +264,37 @@ public static void ConnectionResiliencyTest() } builder.ConnectRetryCount = 2; + // Also check SPID changes with connection resiliency using (SqlConnection conn = new SqlConnection(builder.ConnectionString)) { conn.Open(); + int clientSPID = conn.ServerProcessId; + int serverSPID = 0; InternalConnectionWrapper wrapper = new InternalConnectionWrapper(conn, true, builder.ConnectionString); using (SqlCommand cmd = conn.CreateCommand()) { - cmd.CommandText = "SELECT TOP 1 * FROM dbo.Employees"; + cmd.CommandText = "SELECT @@SPID"; using (SqlDataReader reader = cmd.ExecuteReader()) while (reader.Read()) - { } + { + serverSPID = reader.GetInt16(0); + } + + Assert.Equal(serverSPID, clientSPID); + // Also check SPID after query execution + Assert.Equal(serverSPID, conn.ServerProcessId); wrapper.KillConnectionByTSql(); // Connection resiliency should reconnect transparently using (SqlDataReader reader = cmd.ExecuteReader()) while (reader.Read()) - { } + { + serverSPID = reader.GetInt16(0); + } + + // SPID should match server's SPID + Assert.Equal(serverSPID, conn.ServerProcessId); } } }