From 4285e5931e6a23a7fc2870709474e662efb71259 Mon Sep 17 00:00:00 2001
From: Edward Neal <55035479+edwardneal@users.noreply.github.com>
Date: Fri, 22 May 2026 22:29:23 +0100
Subject: [PATCH 1/4] RAII primitive scaffolding
* Add an UnescapedName property to DatabaseObject and use it in XEventsTracingTest.
* Create ServerLogin primitive.
* Create DatabaseUser primitive.
---
.../DatabaseObjects/DatabaseObject.cs | 2 +
.../Fixtures/DatabaseObjects/DatabaseUser.cs | 58 ++++++++++++
.../Fixtures/DatabaseObjects/ServerLogin.cs | 90 +++++++++++++++++++
.../TracingTests/XEventsTracingTest.cs | 2 +-
4 files changed, 151 insertions(+), 1 deletion(-)
create mode 100644 src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseUser.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ServerLogin.cs
diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs
index 2dc539b5f5..8e913cfb41 100644
--- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs
+++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs
@@ -20,6 +20,8 @@ public abstract class DatabaseObject : IDisposable
public string Name { get; }
+ public string UnescapedName => Name.Substring(1, Name.Length - 2).Replace("]]", "]");
+
protected DatabaseObject(SqlConnection connection, string name, string definition, bool shouldCreate, bool shouldDrop)
{
_shouldDrop = shouldDrop;
diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseUser.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseUser.cs
new file mode 100644
index 0000000000..21d7a4839b
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseUser.cs
@@ -0,0 +1,58 @@
+// 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.
+
+namespace Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects;
+
+///
+/// A transient database user, created at the start of its scope and dropped when disposed.
+///
+///
+/// This class assumes that the associated server login already exists.
+///
+public sealed class DatabaseUser : DatabaseObject
+{
+ private readonly string _databaseName;
+
+ ///
+ /// Initializes a new instance of the DatabaseUser class using the specified SQL connection
+ /// and associated server login.
+ ///
+ /// The SQL connection used to interact with the database.
+ /// The server login which the database user will be associated with.
+ public DatabaseUser(SqlConnection connection, ServerLogin login)
+ : base(connection, login.Name, $"FOR LOGIN {login.Name}", shouldCreate: true, shouldDrop: true)
+ {
+ _databaseName = Connection.Database;
+ }
+
+ protected override void CreateObject(string definition)
+ {
+ using SqlCommand createCommand = new($"CREATE USER {Name} {definition}", Connection);
+
+ createCommand.ExecuteNonQuery();
+ }
+
+ protected override void DropObject()
+ {
+ string? originalDatabase = _databaseName == Connection.Database ? null : _databaseName;
+ using SqlCommand dropCommand = new($"IF USER_ID('{UnescapedName}') IS NOT NULL DROP USER {Name}", Connection);
+
+ try
+ {
+ if (originalDatabase is not null)
+ {
+ Connection.ChangeDatabase(_databaseName);
+ }
+
+ dropCommand.ExecuteNonQuery();
+ }
+ finally
+ {
+ if (originalDatabase is not null)
+ {
+ Connection.ChangeDatabase(originalDatabase);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ServerLogin.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ServerLogin.cs
new file mode 100644
index 0000000000..e668a3c8d3
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ServerLogin.cs
@@ -0,0 +1,90 @@
+// 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.
+
+namespace Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects;
+
+///
+/// A transient server login, created at the start of its scope and dropped when disposed.
+///
+public sealed class ServerLogin : DatabaseObject
+{
+ public string Password { get; }
+
+ ///
+ /// Initializes a new instance of the ServerLogin class using the specified SQL connection, login name prefix, and default database.
+ /// The login will be created with a randomly generated password that meets SQL Server's password complexity requirements.
+ ///
+ /// The SQL connection used to interact with the database.
+ /// The prefix for the login name.
+ /// The default database for the login. If null, not set.
+ public ServerLogin(SqlConnection connection, string namePrefix, string? defaultDatabase = null)
+ : this(connection, GenerateLongName(namePrefix), GeneratePassword(), defaultDatabase)
+ {
+ }
+
+ private ServerLogin(SqlConnection connection, string namePrefix, string password, string? defaultDatabase)
+ : base(connection, namePrefix, GenerateDefinition(password, defaultDatabase), shouldCreate: true, shouldDrop: true)
+ {
+ Password = password;
+ }
+
+ private static string GenerateDefinition(string password, string? defaultDatabase) =>
+ $"WITH PASSWORD='{password}'" +
+ (string.IsNullOrEmpty(defaultDatabase) ? string.Empty : $", DEFAULT_DATABASE=[{defaultDatabase}]");
+
+ ///
+ /// Generates a password which meets the SQL Server password complexity requirements, which are:
+ ///
+ /// Minimum length of 8 characters
+ /// Must contain characters from three of the following four categories:
+ ///
+ /// Uppercase letters (A-Z)
+ /// Lowercase letters (a-z)
+ /// Digits (0-9)
+ /// Non-alphanumeric characters (e.g. !, $, #, %)
+ ///
+ ///
+ ///
+ /// A compliant password.
+ private static string GeneratePassword()
+ {
+ const int PasswordLength = 16;
+ const char UpperCaseStart = 'A';
+ const char LowerCaseStart = 'a';
+ const char DigitsStart = '0';
+
+ // First 5 characters are uppercase letters, next 5 are lowercase letters, and the last 6 are digits
+ Span passwordDigits = stackalloc char[PasswordLength];
+ Random rnd = new();
+
+ for(int i = 0; i < 5; i++)
+ {
+ passwordDigits[i] = (char)(UpperCaseStart + rnd.Next(26));
+ }
+ for (int i = 5; i < 10; i++)
+ {
+ passwordDigits[i] = (char)(LowerCaseStart + rnd.Next(26));
+ }
+ for (int i = 10; i < PasswordLength; i++)
+ {
+ passwordDigits[i] = (char)(DigitsStart + rnd.Next(10));
+ }
+
+ return passwordDigits.ToString();
+ }
+
+ protected override void CreateObject(string definition)
+ {
+ using SqlCommand createCommand = new($"CREATE LOGIN {Name} {definition}", Connection);
+
+ createCommand.ExecuteNonQuery();
+ }
+
+ protected override void DropObject()
+ {
+ using SqlCommand dropCommand = new($"IF SUSER_ID('{UnescapedName}') IS NOT NULL DROP LOGIN {Name}", Connection);
+
+ dropCommand.ExecuteNonQuery();
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs
index b03fc8b224..82a023b5fa 100644
--- a/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs
@@ -120,7 +120,7 @@ public void XEventActivityIDConsistentWithTracing_RpcStarting()
// Our stored procedure name is an escaped SQL Server object name. This will not match the object_name data
// in the XEvent XML, which records it as an unescaped name.
- string unescapedProcedureName = sp.Name.Substring(1, sp.Name.Length - 2).Replace("]]", "]");
+ string unescapedProcedureName = sp.UnescapedName;
VerifyXEventActivityIDConsistentWithTracing(unescapedProcedureName, System.Data.CommandType.StoredProcedure, "rpc_starting");
}
From 7d6f87fb7a1b4dbd340e72718e4853f415b57f3a Mon Sep 17 00:00:00 2001
From: Edward Neal <55035479+edwardneal@users.noreply.github.com>
Date: Fri, 22 May 2026 22:43:05 +0100
Subject: [PATCH 2/4] Add relevant test conditions
---
.../ManualTests/DataCommon/DataTestUtility.cs | 42 +++++++++++++++++++
1 file changed, 42 insertions(+)
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs
index 1e5458e130..7f8fe9bda4 100644
--- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs
@@ -98,6 +98,10 @@ public static class DataTestUtility
private static bool? s_isVectorSupported;
private static bool? s_isVectorFloat16Supported;
+ // Login permissions
+ private static bool? s_isSysAdmin;
+ private static bool? s_isSecurityAdmin;
+
// Azure Synapse EngineEditionId == 6
// More could be read at https://learn.microsoft.com/en-us/sql/t-sql/functions/serverproperty-transact-sql?view=sql-server-ver16#propertyname
public static bool IsAzureSynapse
@@ -231,6 +235,20 @@ private static bool CheckVectorFloat16Supported()
}
}
+ public static bool IsSysAdmin =>
+ s_isSysAdmin ??= IsTCPConnStringSetup() &&
+ IsServerRoleMember("sysadmin");
+
+ public static bool IsSecurityAdmin =>
+ s_isSecurityAdmin ??= IsTCPConnStringSetup() &&
+ IsServerRoleMember("securityadmin");
+
+ public static bool CanCreateLogins =>
+ IsSysAdmin || IsSecurityAdmin;
+
+ public static bool CanUseSqlAuthentication =>
+ IsSysAdmin && GetAuthenticationMode() == 2;
+
static DataTestUtility()
{
Config c = Config.Load();
@@ -531,6 +549,30 @@ public static bool IsTypePresent(string typeName)
return (int)command.ExecuteScalar() > 0;
}
+ public static bool IsServerRoleMember(string roleName)
+ {
+ using SqlConnection connection = new(TCPConnectionString);
+ using SqlCommand command = new("SELECT IS_SRVROLEMEMBER(@role)", connection);
+
+ connection.Open();
+ command.Parameters.AddWithValue("@role", roleName);
+
+ // IS_SRVROLEMEMBER returns 1 if the caller is a member of the specified server role, 0 if not, and DBNull.Value if the role is not valid.
+ return command.ExecuteScalar() is int result && result == 1;
+ }
+
+ public static int GetAuthenticationMode()
+ {
+ using SqlConnection connection = new(TCPConnectionString);
+
+ connection.Open();
+ using SqlCommand command = new("EXEC xp_instance_regread N'HKEY_LOCAL_MACHINE', N'Software\\Microsoft\\MSSQLServer\\MSSQLServer', N'LoginMode'", connection);
+ using SqlDataReader reader = command.ExecuteReader();
+
+ reader.Read();
+ return reader.GetInt32(1);
+ }
+
public static bool IsAdmin
{
get
From f03e34fe7017c82f1929bd5418c93aad16e36274 Mon Sep 17 00:00:00 2001
From: Edward Neal <55035479+edwardneal@users.noreply.github.com>
Date: Sat, 23 May 2026 16:57:22 +0100
Subject: [PATCH 3/4] Modify DatabaseObject to support state
This makes DatabaseUser simpler to work with. It can store a reference to the database the user should be created in, and the class can handle switching to/from that database.
---
.../DatabaseObjects/DatabaseObject.cs | 23 ++++++++++++++--
.../Fixtures/DatabaseObjects/DatabaseUser.cs | 26 ++++++++++++-------
.../Fixtures/DatabaseObjects/ServerLogin.cs | 7 +++--
3 files changed, 40 insertions(+), 16 deletions(-)
diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs
index 8e913cfb41..d913724b50 100644
--- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs
+++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs
@@ -12,21 +12,28 @@ namespace Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects;
/// Base class for a transient database object (such as a table, type or
/// stored procedure.)
///
-public abstract class DatabaseObject : IDisposable
+///
+/// The type of the internal state accessible to derived types at the point of object creation
+/// via the property.
+///
+public abstract class DatabaseObject : IDisposable
{
private readonly bool _shouldDrop;
protected SqlConnection Connection { get; }
+ protected TState State { get; }
+
public string Name { get; }
public string UnescapedName => Name.Substring(1, Name.Length - 2).Replace("]]", "]");
- protected DatabaseObject(SqlConnection connection, string name, string definition, bool shouldCreate, bool shouldDrop)
+ protected DatabaseObject(SqlConnection connection, string name, string definition, TState state, bool shouldCreate, bool shouldDrop)
{
_shouldDrop = shouldDrop;
Connection = connection;
+ State = state;
Name = name;
if (shouldCreate)
@@ -263,3 +270,15 @@ public void Dispose()
GC.SuppressFinalize(this);
}
}
+
+///
+/// Base class for a transient database object (such as a table, type or
+/// stored procedure.)
+///
+public abstract class DatabaseObject : DatabaseObject