diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs
index 14c2609cf3..9e83d58793 100644
--- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs
+++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs
@@ -168,20 +168,8 @@ internal override void CreatePhysicalSNIHandle(
string hostNameInCertificate,
string serverCertificateFilename)
{
- if (isIntegratedSecurity)
- {
- // now allocate proper length of buffer
- if (!string.IsNullOrEmpty(serverSPN))
- {
- // Native SNI requires the Unicode encoding and any other encoding like UTF8 breaks the code.
- SqlClientEventSource.Log.TryTraceEvent("<{0}.{1}|SEC> Server SPN `{2}` from the connection string is used.", nameof(TdsParserStateObjectNative), nameof(CreatePhysicalSNIHandle), serverSPN);
- }
- else
- {
- // This will signal to the interop layer that we need to retrieve the SPN
- serverSPN = string.Empty;
- }
- }
+ // Normalize SPN based on authentication mode
+ serverSPN = NormalizeServerSpn(serverSPN, isIntegratedSecurity);
ConsumerInfo myInfo = CreateConsumerInfo(async);
SQLDNSInfo cachedDNSInfo;
@@ -189,7 +177,44 @@ internal override void CreatePhysicalSNIHandle(
_sessionHandle = new SNIHandle(myInfo, serverName, ref serverSPN, timeout.MillisecondsRemainingInt, out instanceName,
flushCache, !async, fParallel, ipPreference, cachedDNSInfo, hostNameInCertificate);
- resolvedSpn = new(serverSPN.TrimEnd());
+
+ // Only produce resolvedSpn when we actually have one.
+ if (!string.IsNullOrWhiteSpace(serverSPN))
+ {
+ resolvedSpn = new(serverSPN.TrimEnd());
+ }
+ else
+ {
+ resolvedSpn = default;
+ }
+ }
+
+ ///
+ /// Normalizes the serverSPN based on authentication mode.
+ ///
+ /// The server SPN value from the connection string.
+ /// Indicates whether integrated security (SSPI) is being used.
+ ///
+ /// For integrated security: returns if provided, otherwise to trigger SPN generation.
+ /// For SQL auth: returns if is empty (no generation), otherwise returns the provided value.
+ ///
+ internal static string NormalizeServerSpn(string serverSPN, bool isIntegratedSecurity)
+ {
+ if (isIntegratedSecurity)
+ {
+ if (string.IsNullOrWhiteSpace(serverSPN))
+ {
+ // Empty signifies to interop layer that SPN needs to be generated
+ return string.Empty;
+ }
+
+ // Native SNI requires the Unicode encoding and any other encoding like UTF8 breaks the code.
+ SqlClientEventSource.Log.TryTraceEvent(" Server SPN `{0}` from the connection string is used.", serverSPN);
+ return serverSPN;
+ }
+
+ // For SQL auth (and other non-SSPI modes), null means "No SPN generation".
+ return string.IsNullOrWhiteSpace(serverSPN) ? null : serverSPN;
}
protected override uint SniPacketGetData(PacketHandle packet, byte[] _inBuff, ref uint dataSize)
diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs
new file mode 100644
index 0000000000..5315a99d96
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs
@@ -0,0 +1,35 @@
+// 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.
+
+#if NETCOREAPP && WINDOWS
+
+#nullable enable
+
+using Xunit;
+
+namespace Microsoft.Data.SqlClient.UnitTests
+{
+ public class TdsParserStateObjectNativeTests
+ {
+ [Theory]
+ [InlineData(null, true, "")] // Integrated + null -> empty (generate SPN)
+ [InlineData("", true, "")] // Integrated + empty -> empty (generate SPN)
+ [InlineData(" ", true, "")] // Integrated + whitespace -> empty (generate SPN)
+ [InlineData("MSSQLSvc/host", true, "MSSQLSvc/host")] // Integrated + provided -> use it
+ [InlineData(null, false, null)] // SQL Auth + null -> null (no generation)
+ [InlineData("", false, null)] // SQL Auth + empty -> null (no generation)
+ [InlineData(" ", false, null)] // SQL Auth + whitespace -> null (no generation)
+ [InlineData("MSSQLSvc/host", false, "MSSQLSvc/host")] // SQL Auth + provided -> use it
+ public void NormalizeServerSpn_ReturnsExpectedValue(
+ string? inputSpn,
+ bool isIntegratedSecurity,
+ string? expectedSpn)
+ {
+ string? result = TdsParserStateObjectNative.NormalizeServerSpn(inputSpn, isIntegratedSecurity);
+ Assert.Equal(expectedSpn, result);
+ }
+ }
+}
+
+#endif