diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 07b6c9d..8768edf 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ CS1591;CS0649;CS8632;NU1608;NU1109 - 12.1.1 + 12.1.2 preview 1.0.0 SqlServer, Verify diff --git a/src/Tests/Tests.cs b/src/Tests/Tests.cs index 3d6202b..e9e6dab 100644 --- a/src/Tests/Tests.cs +++ b/src/Tests/Tests.cs @@ -852,10 +852,11 @@ await Verify(connection) .SchemaFilter(_ => _.Name is "MyTable" or "MyView" or "MyProcedure"); } - // Verifies the workaround for SMO 181.15.0 + SqlClient 7.0 TypeLoadException. - // SMO's ServerConnection(SqlConnection) constructor references SqlAuthenticationMethod - // which moved from Microsoft.Data.SqlClient to Extensions.Abstractions in SqlClient 7.0. - // The fix avoids that constructor by using reflection to set the SqlConnection directly. + // Exercises Verify(SqlConnection) with an already-open connection. + // Note: this test uses LocalDb (Windows auth) so it passes even if the + // SqlAuthenticationMethod workaround is removed — Windows auth does not + // trigger the SMO type-load for SqlAuthenticationMethod. + // See SqlConnectionObjectFieldWorkaround for the regression guard. [Test] public async Task SchemaFromOpenConnection() { @@ -866,4 +867,26 @@ await Verify(connection) .SchemaFilter(_ => _.Name == "MyTable") .SchemaIncludes(DbObjects.Tables); } + + // Regression: commit fbfa399 removed SqlConnectionObjectField from SqlScriptBuilder, + // which broke Verify(SqlConnection) for connections using SQL Server authentication. + // When SMO 181.x + SqlClient 7.x opens a new connection from a SQL-auth connection + // string it tries to load SqlAuthenticationMethod — a type moved in SqlClient 7.0 — + // causing a TypeLoadException surfaced as "Login failed for user 'sa'". + // The fix injects the already-open SqlConnection directly into SMO via reflection, + // so SMO reuses it and never runs the SQL-auth type-load code path. + [Test] + public void SqlConnectionObjectFieldWorkaround() + { + var field = SqlScriptBuilder.SqlConnectionObjectField; + Assert.That(field, Is.Not.Null, + "SqlConnectionObjectField must exist to work around SMO+SqlClient 7.x TypeLoadException for SQL Server auth connections"); + Assert.That(field.Name, Is.EqualTo("m_SqlConnectionObject")); + + // Verify the field can actually be set on a ServerConnection instance + var serverConnection = new ServerConnection { NonPooledConnection = true }; + using var sqlConnection = new SqlConnection("Server=.;Database=test;Integrated Security=True"); + field.SetValue(serverConnection, sqlConnection); + Assert.That(field.GetValue(serverConnection), Is.SameAs(sqlConnection)); + } } diff --git a/src/Verify.SqlServer/GlobalUsings.cs b/src/Verify.SqlServer/GlobalUsings.cs index 4af539c..cba6437 100644 --- a/src/Verify.SqlServer/GlobalUsings.cs +++ b/src/Verify.SqlServer/GlobalUsings.cs @@ -1,4 +1,5 @@ global using System.Data; +global using System.Reflection; global using System.Data.Common; global using System.Data.SqlTypes; global using System.Globalization; diff --git a/src/Verify.SqlServer/SchemaValidation/SqlScriptBuilder.cs b/src/Verify.SqlServer/SchemaValidation/SqlScriptBuilder.cs index 18c1bf2..6f3ec3c 100644 --- a/src/Verify.SqlServer/SchemaValidation/SqlScriptBuilder.cs +++ b/src/Verify.SqlServer/SchemaValidation/SqlScriptBuilder.cs @@ -1,5 +1,20 @@ class SqlScriptBuilder(SchemaSettings settings) { + // TODO: when Microsoft.Data.SqlClient adds TypeForwardedTo for SqlAuthenticationMethod, + // revert to using new ServerConnection(SqlConnection) and remove this reflection workaround. + // + // SMO 181.15.0 ServerConnection(SqlConnection) constructor calls InitFromSqlConnection + // which references SqlAuthenticationMethod — a type moved from Microsoft.Data.SqlClient + // to Microsoft.Data.SqlClient.Extensions.Abstractions in SqlClient 7.0. The CLR can't + // resolve the type in the original assembly, causing a TypeLoadException. + // + // Workaround: construct ServerConnection() with default constructor (no InitFromSqlConnection), + // then set the internal m_SqlConnectionObject field via reflection to reuse the open connection. + // SMO detects the connection is already open and uses it directly. + internal static readonly FieldInfo SqlConnectionObjectField = + typeof(ConnectionManager).GetField("m_SqlConnectionObject", BindingFlags.NonPublic | BindingFlags.Instance) ?? + throw new("Could not find field m_SqlConnectionObject on ConnectionManager. The SMO internals may have changed."); + static Dictionary tableSettingsToScrubLookup; static SqlScriptBuilder() @@ -35,6 +50,7 @@ public string BuildContent(SqlConnection connection) }; try { + SqlConnectionObjectField.SetValue(serverConnection, connection); var server = new Server(serverConnection); return BuildContent(server, builder); }