diff --git a/.azuredevops/policies/approvercountpolicy.yml b/.azuredevops/policies/approvercountpolicy.yml index 78e0c2aa0f..e8175f63fa 100644 --- a/.azuredevops/policies/approvercountpolicy.yml +++ b/.azuredevops/policies/approvercountpolicy.yml @@ -3,10 +3,10 @@ # that govern how PRs are approved in general. The settings here dictate how # the validator behaves, and it can also prevent PRs from completing. # -# Suggested by Merlinbot (https://sqlclientdrivers.visualstudio.com/ADO.Net/_git/dotnet-sqlclient/pullrequest/4982) +# https://eng.ms/docs/coreai/devdiv/one-engineering-system-1es/1es-docs/policy-service/policy-as-code/approver-count-policy-overview name: approver_count -description: Approver count policy for dotnet-sqlclient +description: Approver count policy for dotnet-sqlclient [internal/main] resource: repository where: configuration: @@ -22,8 +22,5 @@ configuration: resetRejectionsOnSourcePush: false blockLastPusherVote: true branchNames: - - refs/heads/internal/main - - refs/heads/internal/release/6.0 - - refs/heads/internal/release/5.2 - - refs/heads/internal/release/5.1 - displayName: dotnet-sqlclient Approver Count Policy + - internal/main + displayName: dotnet-sqlclient Approver Count Policy [internal/main] diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index 916b9ac04d..7b1fb3bc30 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -7,7 +7,6 @@ using System.Data; using System.Data.SqlTypes; using System.Diagnostics.Tracing; -using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -18,6 +17,7 @@ using System.Security; using System.Security.Principal; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Azure.Core; @@ -25,6 +25,7 @@ using Microsoft.Data.SqlClient.TestUtilities; using Microsoft.Identity.Client; using Xunit; +using Xunit.Abstractions; namespace Microsoft.Data.SqlClient.ManualTesting.Tests { @@ -100,7 +101,7 @@ public static bool IsAzureSynapse { if (!string.IsNullOrEmpty(TCPConnectionString)) { - s_sqlServerEngineEdition ??= GetSqlServerProperty(TCPConnectionString, "EngineEdition"); + s_sqlServerEngineEdition ??= GetSqlServerProperty(TCPConnectionString, ServerProperty.EngineEdition); } _ = int.TryParse(s_sqlServerEngineEdition, out int engineEditon); return engineEditon == 6; @@ -122,7 +123,7 @@ public static string SQLServerVersion { if (!string.IsNullOrEmpty(TCPConnectionString)) { - s_sQLServerVersion ??= GetSqlServerProperty(TCPConnectionString, "ProductMajorVersion"); + s_sQLServerVersion ??= GetSqlServerProperty(TCPConnectionString, ServerProperty.ProductMajorVersion); } return s_sQLServerVersion; } @@ -235,7 +236,9 @@ public static IEnumerable GetConnectionStrings(bool withEnclave) yield return TCPConnectionString; } // Named Pipes are not supported on Unix platform and for Azure DB - if (Environment.OSVersion.Platform != PlatformID.Unix && IsNotAzureServer() && !string.IsNullOrEmpty(NPConnectionString)) + if (Environment.OSVersion.Platform != PlatformID.Unix && + IsNotAzureServer() && + !string.IsNullOrEmpty(NPConnectionString)) { yield return NPConnectionString; } @@ -286,29 +289,99 @@ private static Task AcquireTokenAsync(string authorityURL, string userID public static bool IsKerberosTest => !string.IsNullOrEmpty(KerberosDomainUser) && !string.IsNullOrEmpty(KerberosDomainPassword); - public static string GetSqlServerProperty(string connectionString, string propertyName) + #nullable enable + + /// + /// Returns the current test name as: + /// + /// ClassName.MethodName + /// + /// xUnit v2 doesn't provide access to a test context, so we use + /// reflection into the ITestOutputHelper to get the test name. + /// + /// + /// + /// The output helper instance for the currently running test. + /// + /// + /// The current test name. + public static string CurrentTestName(ITestOutputHelper outputHelper) + { + // Reflect our way to the ITest instance. + var type = outputHelper.GetType(); + Assert.NotNull(type); + var testMember = type.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(testMember); + var test = testMember.GetValue(outputHelper) as ITest; + Assert.NotNull(test); + + // The DisplayName is in the format: + // + // Namespace.ClassName.MethodName(args) + // + // We only want the ClassName.MethodName portion. + // + Match match = TestNameRegex.Match(test.DisplayName); + Assert.True(match.Success); + // There should be 2 groups: the overall match, and the capture + // group. + Assert.Equal(2, match.Groups.Count); + + // The portion we want is in the capture group. + return match.Groups[1].Value; + } + + private static readonly Regex TestNameRegex = new( + // Capture the ClassName.MethodName portion, which may terminate + // the name, or have (args...) appended. + @"\.((?:[^.]+)\.(?:[^.\(]+))(?:\(.*\))?$", + RegexOptions.Compiled); + + /// + /// SQL Server properties we can query. + /// + /// GOTCHA: The enum member names must match the property names + /// queryable via T-SQL SERVERPROPERTY(). See: + /// + /// https://learn.microsoft.com/en-us/sql/t-sql/functions/serverproperty-transact-sql + /// + public enum ServerProperty + { + ProductMajorVersion, + EngineEdition + } + + public static string GetSqlServerProperty(string connectionString, ServerProperty property) { - string propertyValue = string.Empty; using SqlConnection conn = new(connectionString); conn.Open(); - SqlCommand command = conn.CreateCommand(); - command.CommandText = $"SELECT SERVERProperty('{propertyName}')"; - SqlDataReader reader = command.ExecuteReader(); - if (reader.Read()) + return GetSqlServerProperty(conn, property); + } + + public static string GetSqlServerProperty(SqlConnection connection, ServerProperty property) + { + using SqlCommand command = connection.CreateCommand(); + command.CommandText = $"SELECT SERVERProperty('{property}')"; + using SqlDataReader reader = command.ExecuteReader(); + + Assert.True(reader.Read()); + + switch (property) { - switch (propertyName) - { - case "EngineEdition": - propertyValue = reader.GetInt32(0).ToString(); - break; - case "ProductMajorVersion": - propertyValue = reader.GetString(0); - break; - } + case ServerProperty.EngineEdition: + // EngineEdition is returned as an int. + return reader.GetInt32(0).ToString(); + case ServerProperty.ProductMajorVersion: + default: + // ProductMajorVersion is returned as a string. + // + // Assume any unknown property is also a string. + return reader.GetString(0); } - return propertyValue; } + #nullable disable + public static bool GetSQLServerStatusOnTDS8(string connectionString) { bool isTDS8Supported = false; @@ -1220,69 +1293,102 @@ protected virtual void OnMatchingEventWritten(EventWrittenEventArgs eventData) } } + #nullable enable + public readonly ref struct XEventScope : IDisposable { - private const int MaxXEventsLatencyS = 5; + #region Private Fields + // Maximum dispatch latency for XEvents, in seconds. + private const int MaxDispatchLatencySeconds = 5; + + // The connection to use for all operations. private readonly SqlConnection _connection; - private readonly bool _useDatabaseSession; - public string SessionName { get; } + // True if connected to an Azure SQL instance. + private readonly bool _isAzureSql; - public XEventScope(SqlConnection connection, string eventSpecification, string targetSpecification) - { - SessionName = GenerateRandomCharacters("Session"); - _connection = connection; - // SQL Azure only supports database-scoped XEvent sessions - _useDatabaseSession = GetSqlServerProperty(connection.ConnectionString, "EngineEdition") == "5"; + // True if connected to a non-Azure SQL Server 2025 (version 17) or + // higher. + private readonly bool _isVersion17OrHigher; - SetupXEvent(eventSpecification, targetSpecification); - } + // Duration for the XEvent session, in minutes. + private readonly ushort _durationInMinutes; - public void Dispose() - => DropXEvent(); + #endregion - public System.Xml.XmlDocument GetEvents() - { - string xEventQuery = _useDatabaseSession - ? $@"SELECT xet.target_data - FROM sys.dm_xe_database_session_targets AS xet - INNER JOIN sys.dm_xe_database_sessions AS xe - ON (xe.address = xet.event_session_address) - WHERE xe.name = '{SessionName}'" - : $@"SELECT xet.target_data - FROM sys.dm_xe_session_targets AS xet - INNER JOIN sys.dm_xe_sessions AS xe - ON (xe.address = xet.event_session_address) - WHERE xe.name = '{SessionName}'"; + #region Properties - using (SqlCommand command = new SqlCommand(xEventQuery, _connection)) - { - Thread.Sleep(MaxXEventsLatencyS * 1000); + /// + /// The name of the XEvent session, derived from the session name + /// provided at construction time, with a unique suffix appended. + /// + public string SessionName { get; } - if (_connection.State == ConnectionState.Closed) - { - _connection.Open(); - } + #endregion + + #region Construction + + /// + /// Construct with the specified parameters. + /// + /// This will use the connection to query the server properties and + /// setup and start the XEvent session. + /// + /// The base name of the session. + /// The SQL connection to use. (Must already be open.) + /// The event specification T-SQL string. + /// The target specification T-SQL string. + /// The duration of the session in minutes. + public XEventScope( + string sessionName, + // The connection must already be open. + SqlConnection connection, + string eventSpecification, + string targetSpecification, + ushort durationInMinutes = 5) + { + SessionName = GenerateRandomCharacters(sessionName); + + _connection = connection; + Assert.Equal(ConnectionState.Open, _connection.State); + + _durationInMinutes = durationInMinutes; - string targetData = command.ExecuteScalar() as string; - System.Xml.XmlDocument xmlDocument = new System.Xml.XmlDocument(); + // EngineEdition 5 indicates Azure SQL. + _isAzureSql = GetSqlServerProperty(connection, ServerProperty.EngineEdition) == "5"; - xmlDocument.LoadXml(targetData); - return xmlDocument; + // Determine if we're connected to a SQL Server instance version + // 17 or higher. + if (!_isAzureSql) + { + int majorVersion; + Assert.True( + int.TryParse( + GetSqlServerProperty(connection, ServerProperty.ProductMajorVersion), + out majorVersion)); + _isVersion17OrHigher = majorVersion >= 17; } - } - private void SetupXEvent(string eventSpecification, string targetSpecification) - { - string sessionLocation = _useDatabaseSession ? "DATABASE" : "SERVER"; - string xEventCreateAndStartCommandText = $@"CREATE EVENT SESSION [{SessionName}] ON {sessionLocation} + // Setup and start the XEvent session. + string sessionLocation = _isAzureSql ? "DATABASE" : "SERVER"; + + // Both Azure SQL and SQL Server 2025+ support setting a maximum + // duration for the XEvent session. + string duration = + _isAzureSql || _isVersion17OrHigher + ? $"MAX_DURATION={_durationInMinutes} MINUTES," + : string.Empty; + + string xEventCreateAndStartCommandText = + $@"CREATE EVENT SESSION [{SessionName}] ON {sessionLocation} {eventSpecification} {targetSpecification} WITH ( + {duration} MAX_MEMORY=16 MB, EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS, - MAX_DISPATCH_LATENCY={MaxXEventsLatencyS} SECONDS, + MAX_DISPATCH_LATENCY={MaxDispatchLatencySeconds} SECONDS, MAX_EVENT_SIZE=0 KB, MEMORY_PARTITION_MODE=NONE, TRACK_CAUSALITY=ON, @@ -1290,30 +1396,88 @@ private void SetupXEvent(string eventSpecification, string targetSpecification) ALTER EVENT SESSION [{SessionName}] ON {sessionLocation} STATE = START "; - using (SqlCommand createXEventSession = new SqlCommand(xEventCreateAndStartCommandText, _connection)) - { - if (_connection.State == ConnectionState.Closed) - { - _connection.Open(); - } + using SqlCommand createXEventSession = new SqlCommand(xEventCreateAndStartCommandText, _connection); + createXEventSession.ExecuteNonQuery(); + } + + /// + /// Disposal stops and drops the XEvent session. + /// + /// + /// Disposal isn't perfect - tests can abort without cleaning up the + /// events they have created. For Azure SQL targets that outlive the + /// test pipelines, it is beneficial to periodically log into the + /// database and drop old XEvent sessions using T-SQL similar to + /// this: + /// + /// DECLARE @sql NVARCHAR(MAX) = N''; + /// + /// -- Identify inactive (stopped) event sessions and generate DROP commands + /// SELECT @sql += N'DROP EVENT SESSION [' + name + N'] ON SERVER;' + CHAR(13) + CHAR(10) + /// FROM sys.server_event_sessions + /// WHERE running = 0; -- Filter for sessions that are not running (inactive) + /// + /// -- Print the generated commands for review (optional, but recommended) + /// PRINT @sql; + /// + /// -- Execute the generated commands + /// EXEC sys.sp_executesql @sql; + /// + public void Dispose() + { + string dropXEventSessionCommand = _isAzureSql + // We choose the sys.(database|server)_event_sessions views + // here to ensure we find sessions that may not be running. + ? $"IF EXISTS (select * from sys.database_event_sessions where name ='{SessionName}')" + + $" DROP EVENT SESSION [{SessionName}] ON DATABASE" + : $"IF EXISTS (select * from sys.server_event_sessions where name ='{SessionName}')" + + $" DROP EVENT SESSION [{SessionName}] ON SERVER"; - createXEventSession.ExecuteNonQuery(); - } + using SqlCommand command = new SqlCommand(dropXEventSessionCommand, _connection); + command.ExecuteNonQuery(); } - private void DropXEvent() + #endregion + + #region Public Methods + + /// + /// Query the XEvent session for its collected events, returning + /// them as an XML document. + /// + /// This always blocks the thread for MaxDispatchLatencySeconds to + /// ensure that all events have been flushed into the ring buffer. + /// + public System.Xml.XmlDocument GetEvents() { - string dropXEventSessionCommand = _useDatabaseSession - ? $"IF EXISTS (select * from sys.dm_xe_database_sessions where name ='{SessionName}')" + - $" DROP EVENT SESSION [{SessionName}] ON DATABASE" - : $"IF EXISTS (select * from sys.dm_xe_sessions where name ='{SessionName}')" + - $" DROP EVENT SESSION [{SessionName}] ON SERVER"; + string xEventQuery = _isAzureSql + ? $@"SELECT xet.target_data + FROM sys.dm_xe_database_session_targets AS xet + INNER JOIN sys.dm_xe_database_sessions AS xe + ON (xe.address = xet.event_session_address) + WHERE xe.name = '{SessionName}'" + : $@"SELECT xet.target_data + FROM sys.dm_xe_session_targets AS xet + INNER JOIN sys.dm_xe_sessions AS xe + ON (xe.address = xet.event_session_address) + WHERE xe.name = '{SessionName}'"; - using (SqlCommand command = new SqlCommand(dropXEventSessionCommand, _connection)) - { - command.ExecuteNonQuery(); - } + using SqlCommand command = new SqlCommand(xEventQuery, _connection); + + // Wait for maximum dispatch latency to ensure all events + // have been flushed to the ring buffer. + Thread.Sleep(MaxDispatchLatencySeconds * 1000); + + string? targetData = command.ExecuteScalar() as string; + Assert.NotNull(targetData); + + System.Xml.XmlDocument xmlDocument = new System.Xml.XmlDocument(); + + xmlDocument.LoadXml(targetData); + return xmlDocument; } + + #endregion } /// @@ -1342,4 +1506,6 @@ public static string GetMachineFQDN(string hostname) return fqdn.ToString(); } } + + #nullable disable } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs index 83507727a1..b4b7271216 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs @@ -15,13 +15,21 @@ using System.Xml; using Microsoft.Data.SqlClient.TestUtilities; using Xunit; +using Xunit.Abstractions; namespace Microsoft.Data.SqlClient.ManualTesting.Tests { - public static class DataStreamTest + public class DataStreamTest { + private readonly string _testName; + + public DataStreamTest(ITestOutputHelper outputHelper) + { + _testName = DataTestUtility.CurrentTestName(outputHelper); + } + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] - public static void RunAllTestsForSingleServer_NP() + public void RunAllTestsForSingleServer_NP() { // @TODO: Split into separate tests! Or why even bother running this test on non-windows, the error comes from something other than data stream! if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -35,7 +43,7 @@ public static void RunAllTestsForSingleServer_NP() } [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] - public static void RunAllTestsForSingleServer_TCP() + public void RunAllTestsForSingleServer_TCP() { RunAllTestsForSingleServer(DataTestUtility.TCPConnectionString); } @@ -155,7 +163,7 @@ IF OBJECT_ID('dbo.{tableName}', 'U') IS NOT NULL } // @TODO: Split into separate tests! - private static void RunAllTestsForSingleServer(string connectionString, bool usingNamePipes = false) + private void RunAllTestsForSingleServer(string connectionString, bool usingNamePipes = false) { RowBuffer(connectionString); InvalidRead(connectionString); @@ -1943,58 +1951,61 @@ private static void VariantCollationsTest(string connectionString) } } - private static void TestXEventsStreaming(string connectionString) + #nullable enable + + private void TestXEventsStreaming(string connectionString) { // Create XEvent - using (SqlConnection xEventManagementConnection = new SqlConnection(connectionString)) - using (DataTestUtility.XEventScope xEventScope = new DataTestUtility.XEventScope(xEventManagementConnection, - "ADD EVENT sqlserver.user_event(ACTION(package0.event_sequence))", - "ADD TARGET package0.ring_buffer")) + using SqlConnection xEventManagementConnection = new SqlConnection(connectionString); + xEventManagementConnection.Open(); + + using DataTestUtility.XEventScope xEventScope = + new DataTestUtility.XEventScope( + _testName, + xEventManagementConnection, + "ADD EVENT sqlserver.user_event(ACTION(package0.event_sequence))", + "ADD TARGET package0.ring_buffer"); + + string sessionName = xEventScope.SessionName; + + Task.Factory.StartNew(() => { - string sessionName = xEventScope.SessionName; + // Read XEvents + int streamXeventCount = 3; + using SqlConnection xEventsReadConnection = new SqlConnection(connectionString); + xEventsReadConnection.Open(); + + string xEventDataStreamCommand = "USE master; " + @"select [type], [data] from sys.fn_MSxe_read_event_stream ('" + sessionName + "',0)"; + using SqlCommand cmd = new SqlCommand(xEventDataStreamCommand, xEventsReadConnection); + using SqlDataReader reader = cmd.ExecuteReader(System.Data.CommandBehavior.SequentialAccess); - Task.Factory.StartNew(() => + for (int i = 0; i < streamXeventCount && reader.Read(); i++) { - // Read XEvents - int streamXeventCount = 3; - using (SqlConnection xEventsReadConnection = new SqlConnection(connectionString)) - { - xEventsReadConnection.Open(); - string xEventDataStreamCommand = "USE master; " + @"select [type], [data] from sys.fn_MSxe_read_event_stream ('" + sessionName + "',0)"; - using (SqlCommand cmd = new SqlCommand(xEventDataStreamCommand, xEventsReadConnection)) - { - SqlDataReader reader = cmd.ExecuteReader(System.Data.CommandBehavior.SequentialAccess); - for (int i = 0; i < streamXeventCount && reader.Read(); i++) - { - int colType = reader.GetInt32(0); - int cb = (int)reader.GetBytes(1, 0, null, 0, 0); + int colType = reader.GetInt32(0); + int cb = (int)reader.GetBytes(1, 0, null, 0, 0); - byte[] bytes = new byte[cb]; - long read = reader.GetBytes(1, 0, bytes, 0, cb); + byte[] bytes = new byte[cb]; + long read = reader.GetBytes(1, 0, bytes, 0, cb); - // Don't send data on the first read because there is already data in the buffer. - // Don't send data on the last iteration. We will not be reading that data. - if (i == 0 || i == streamXeventCount - 1) - { - continue; - } - - using (SqlConnection xEventWriteConnection = new SqlConnection(connectionString)) - { - xEventWriteConnection.Open(); - string xEventWriteCommandText = @"exec sp_trace_generateevent 90, N'Test2'"; - using (SqlCommand xEventWriteCommand = new SqlCommand(xEventWriteCommandText, xEventWriteConnection)) - { - xEventWriteCommand.ExecuteNonQuery(); - } - } - } - } + // Don't send data on the first read because there is already data in the buffer. + // Don't send data on the last iteration. We will not be reading that data. + if (i == 0 || i == streamXeventCount - 1) + { + continue; } - }).Wait(10000); - } + + using SqlConnection xEventWriteConnection = new SqlConnection(connectionString); + xEventWriteConnection.Open(); + + string xEventWriteCommandText = @"exec sp_trace_generateevent 90, N'Test2'"; + using SqlCommand xEventWriteCommand = new SqlCommand(xEventWriteCommandText, xEventWriteConnection); + xEventWriteCommand.ExecuteNonQuery(); + } + }).Wait(10000); } + #nullable disable + private static void TimeoutDuringReadAsyncWithClosedReaderTest(string connectionString) { // Create the proxy diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs index 7990b7de50..bcb23d11bd 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs @@ -7,11 +7,21 @@ using System.Xml; using System.Xml.XPath; using Xunit; +using Xunit.Abstractions; + +#nullable enable namespace Microsoft.Data.SqlClient.ManualTesting.Tests { public class XEventsTracingTest { + private readonly string _testName; + + public XEventsTracingTest(ITestOutputHelper outputHelper) + { + _testName = DataTestUtility.CurrentTestName(outputHelper); + } + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] [InlineData("SELECT @@VERSION", System.Data.CommandType.Text, "sql_statement_starting")] [InlineData("sp_help", System.Data.CommandType.StoredProcedure, "rpc_starting")] @@ -28,10 +38,14 @@ public void XEventActivityIDConsistentWithTracing(string query, System.Data.Comm HashSet ids; using SqlConnection xEventManagementConnection = new(DataTestUtility.TCPConnectionString); - using DataTestUtility.XEventScope xEventSession = new(xEventManagementConnection, + xEventManagementConnection.Open(); + + using DataTestUtility.XEventScope xEventSession = new( + _testName, + xEventManagementConnection, $@"ADD EVENT SQL_STATEMENT_STARTING (ACTION (client_connection_id) WHERE (client_connection_id='{connectionId}')), ADD EVENT RPC_STARTING (ACTION (client_connection_id) WHERE (client_connection_id='{connectionId}'))", - "ADD TARGET ring_buffer"); + "ADD TARGET ring_buffer"); using (DataTestUtility.MDSEventListener TraceListener = new()) { @@ -54,7 +68,9 @@ ADD EVENT RPC_STARTING (ACTION (client_connection_id) WHERE (client_connection_i private static string GetCommandActivityId(string commandText, string eventName, Guid connectionId, XmlDocument xEvents) { - XPathNavigator xPathRoot = xEvents.CreateNavigator(); + XPathNavigator? xPathRoot = xEvents.CreateNavigator(); + Assert.NotNull(xPathRoot); + // The transferred activity ID is attached to the "attach_activity_id_xfer" action within // the "sql_statement_starting" and the "rpc_starting" events. XPathNodeIterator statementStartingQuery = xPathRoot.Select( @@ -65,7 +81,9 @@ private static string GetCommandActivityId(string commandText, string eventName, Assert.Equal(1, statementStartingQuery.Count); Assert.True(statementStartingQuery.MoveNext()); - XPathNavigator activityIdElement = statementStartingQuery.Current.SelectSingleNode("action[@name='attach_activity_id_xfer']/value"); + XPathNavigator? current = statementStartingQuery.Current; + Assert.NotNull(current); + XPathNavigator? activityIdElement = current.SelectSingleNode("action[@name='attach_activity_id_xfer']/value"); Assert.NotNull(activityIdElement); Assert.NotNull(activityIdElement.Value);