From a144ef61b5096ea214f8ce65c2ffecc26b3b0a2c Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Fri, 22 Aug 2025 15:38:09 -0700 Subject: [PATCH 01/19] Add Json Payload Functionality for User Agent Feature Extension --- .../src/Microsoft.Data.SqlClient.csproj | 6 + .../netfx/src/Microsoft.Data.SqlClient.csproj | 6 + .../Data/SqlClient/UserAgent/UserAgentInfo.cs | 356 ++++++++++++++++++ .../SqlClient/UserAgent/UserAgentInfoDto.cs | 53 +++ .../tests/UnitTests/UserAgentInfoTests.cs | 333 ++++++++++++++++ 5 files changed, 754 insertions(+) create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfoDto.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 97f81d7c4b..c3b06897c1 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -789,6 +789,12 @@ Microsoft\Data\SqlTypes\SqlVector.cs + + Microsoft\Data\SqlClient\UserAgent\UserAgentInfo.cs + + + Microsoft\Data\SqlClient\UserAgent\UserAgentInfoDto.cs + Resources\ResCategoryAttribute.cs 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 84ce735592..87e865c4ed 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -900,6 +900,12 @@ Microsoft\Data\SqlTypes\SqlVector.cs + + Microsoft\Data\SqlClient\UserAgent\UserAgentInfo.cs + + + Microsoft\Data\SqlClient\UserAgent\UserAgentInfoDto.cs + Resources\ResDescriptionAttribute.cs diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs new file mode 100644 index 0000000000..d7aa1e1163 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs @@ -0,0 +1,356 @@ +// 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. + +using System; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Data.Common; + +#nullable enable + +namespace Microsoft.Data.SqlClient.UserAgent; + +/// +/// Gathers driver + environment info, enforces size constraints, +/// and serializes into a UTF-8 JSON payload. +/// The spec document can be found at: https://microsoft.sharepoint-df.com/:w:/t/sqldevx/ERIWTt0zlCxLroNHyaPlKYwBI_LNSff6iy_wXZ8xX6nctQ?e=0hTJX7 +/// +internal static class UserAgentInfo +{ + /// + /// Maximum number of characters allowed for the system architecture. + /// + private const int ArchMaxChars = 16; + + /// + /// Maximum number of characters allowed for the driver name. + /// + internal const int DriverNameMaxChars = 16; + + /// + /// Maximum number of bytes allowed for the user agent json payload. + /// Payloads larger than this may be rejected by the server. + /// + internal const int JsonPayloadMaxBytes = 2047; + + /// + /// Maximum number of characters allowed for the operating system details. + /// + private const int OsDetailsMaxChars = 128; + + /// + /// Maximum number of characters allowed for the operating system type. + /// + internal const int OsTypeMaxChars = 16; + + /// + /// Maximum number of characters allowed for the driver runtime. + /// + private const int RuntimeMaxChars = 128; + + /// + /// Maximum number of characters allowed for the driver version. + /// + internal const int VersionMaxChars = 16; + + + internal const string DefaultJsonValue = "Unknown"; + internal const string DriverName = "MS-MDS"; + + private static readonly UserAgentInfoDto s_dto; + private static readonly byte[] s_userAgentCachedPayload; + + /// + /// Provides the UTF-8 encoded UserAgent JSON payload as a cached read-only memory buffer. + /// The value is computed once during process initialization and reused across all calls. + /// No re-encoding or recalculation occurs at access time, and the same memory is safely shared across all threads. + /// + public static ReadOnlyMemory UserAgentCachedJsonPayload => s_userAgentCachedPayload; + + private enum OsType + { + Windows, + Linux, + macOS, + FreeBSD, + Android, + Unknown + } + + static UserAgentInfo() + { + s_dto = BuildDto(); + s_userAgentCachedPayload = AdjustJsonPayloadSize(s_dto); + } + + /// + /// This function returns the appropriately sized json payload + /// We check the size of encoded json payload, if it is within limits we return the dto to be cached + /// other wise we drop some fields to reduce the size of the payload. + /// + /// Data Transfer Object for the json payload + /// Serialized UTF-8 encoded json payload version of DTO within size limit + internal static byte[] AdjustJsonPayloadSize(UserAgentInfoDto dto) + { + // Note: We serialize 6 fields in total: + // - 4 fields with up to 16 characters each + // - 2 fields with up to 128 characters each + // + // For estimating **on-the-wire UTF-8 size** of the serialized JSON: + // 1) For the 4 fields of 16 characters: + // - In worst case (all characters require escaping in JSON, e.g., quotes, backslashes, control chars), + // each character may expand to 2–6 bytes in the JSON string (e.g., \" = 2 bytes, \uXXXX = 6 bytes) + // - Assuming full escape with \uXXXX form (6 bytes per char): 4 × 16 × 6 = 384 bytes (extreme worst case) + // - For unescaped high-plane Unicode (e.g., emojis), UTF-8 uses up to 4 bytes per character: + // 4 × 16 × 4 = 256 bytes (UTF-8 max) + // + // Conservative max estimate for these fields = **384 bytes** + // + // 2) For the 2 fields of 128 characters: + // - Worst-case with \uXXXX escape sequences: 2 × 128 × 6 = 1,536 bytes + // - Worst-case with high Unicode: 2 × 128 × 4 = 1,024 bytes + // + // Conservative max estimate for these fields = **1,536 bytes** + // + // Combined worst-case for value content = 384 + 1536 = **1,920 bytes** + // + // 3) The rest of the serialized JSON payload (object braces, field names, quotes, colons, commas) is fixed. + // Based on measurements, it typically adds to about **81 bytes**. + // + // Final worst-case estimate for total payload on the wire (UTF-8 encoded): + // 1,920 + 81 = **2,001 bytes** + // + // This is still below our spec limit of 2,047 bytes. + // + // TDS Prelogin7 packets support up to 65,535 bytes (including headers), but many server versions impose + // stricter limits for prelogin payloads. + // + // As a safety measure: + // - If the serialized payload exceeds **10 KB**, we fallback to transmitting only essential fields: + // 'driver', 'version', and 'os.type' + // - If the payload exceeds 2,047 bytes but remains within sensible limits, we still send it, but note that + // some servers may silently drop or reject such packets — behavior we may use for future probing or diagnostics. + // - If payload exceeds 10KB even after dropping fields , we send an empty payload. + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = null, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false + }; + byte[] payload = JsonSerializer.SerializeToUtf8Bytes(dto, options); + + // We try to send the payload if it is within the limits. + // Otherwise we drop some fields to reduce the size of the payload and try one last time + // Note: server will reject payloads larger than 2047 bytes + // Try if the payload fits the max allowed bytes + if (payload.Length <= JsonPayloadMaxBytes) + { + return payload; + } + + dto.Runtime = null; // drop Runtime + dto.Arch = null; // drop Arch + if (dto.OS != null) + { + dto.OS.Details = null; // drop OS.Details + } + + payload = JsonSerializer.SerializeToUtf8Bytes(dto, options); + if (payload.Length <= JsonPayloadMaxBytes) + { + return payload; + } + + dto.OS = null; // drop OS entirely + // Last attempt to send minimal payload driver + version only + // As per the comment in AdjustJsonPayloadSize, we know driver + version cannot be larger than the max + return JsonSerializer.SerializeToUtf8Bytes(dto, options); + } + + internal static UserAgentInfoDto BuildDto() + { + // Instantiate DTO before serializing + return new UserAgentInfoDto + { + Driver = TruncateOrDefault(DriverName, DriverNameMaxChars), + Version = TruncateOrDefault(ADP.GetAssemblyVersion().ToString(), VersionMaxChars), + OS = new UserAgentInfoDto.OsInfo + { + Type = TruncateOrDefault(DetectOsType().ToString(), OsTypeMaxChars), + Details = TruncateOrDefault(DetectOsDetails(), OsDetailsMaxChars) + }, + Arch = TruncateOrDefault(DetectArchitecture(), ArchMaxChars), + Runtime = TruncateOrDefault(DetectRuntime(), RuntimeMaxChars) + }; + + } + + /// + /// Detects and reports whatever CPU architecture the guest OS exposes + /// + private static string DetectArchitecture() + { + try + { + // Returns the architecture of the current process (e.g., "X86", "X64", "Arm", "Arm64"). + // Note: This reflects the architecture of the running process, not the physical host system. + return RuntimeInformation.ProcessArchitecture.ToString(); + } + catch + { + // In case RuntimeInformation isn’t available or something unexpected happens + return DefaultJsonValue; + } + } + + /// + /// Retrieves the operating system details based on RuntimeInformation. + /// + private static string DetectOsDetails() + { + var osDetails = RuntimeInformation.OSDescription; + if (!string.IsNullOrWhiteSpace(osDetails)) + { + return osDetails; + } + + return DefaultJsonValue; + } + + /// + /// Detects the OS platform and returns the matching OsType enum. + /// + private static OsType DetectOsType() + { + try + { + // first we try with built-in checks (Android and FreeBSD also report Linux so they are checked first) +#if NET6_0_OR_GREATER + if (OperatingSystem.IsAndroid()) + { + return OsType.Android; + } + if (OperatingSystem.IsFreeBSD()) + { + return OsType.FreeBSD; + } + if (OperatingSystem.IsWindows()) + { + return OsType.Windows; + } + if (OperatingSystem.IsLinux()) + { + return OsType.Linux; + } + if (OperatingSystem.IsMacOS()) + { + return OsType.macOS; + } +#endif + +#if NET462 + if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("FREEBSD"))) + { + return OsType.FreeBSD; + } +#else + if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) + { + return OsType.FreeBSD; + } +#endif + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return OsType.Windows; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return OsType.Linux; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return OsType.macOS; + } + + // Final fallback is inspecting OSDecription + // Note: This is not based on any formal specification, + // that is why we use it as a last resort. + // The string values are based on trial and error. + var desc = RuntimeInformation.OSDescription?.ToLowerInvariant() ?? ""; + if (desc.Contains("android")) + { + return OsType.Android; + } + if (desc.Contains("freebsd")) + { + return OsType.FreeBSD; + } + if (desc.Contains("windows")) + { + return OsType.Windows; + } + if (desc.Contains("linux")) + { + return OsType.Linux; + } + if (desc.Contains("darwin") || desc.Contains("mac os")) + { + return OsType.macOS; + } + } + catch + { + // swallow any unexpected errors + return OsType.Unknown; + } + return OsType.Unknown; + } + + /// + /// Returns the framework description as a string. + /// + private static string DetectRuntime() + { + // FrameworkDescription is never null, but IsNullOrWhiteSpace covers it anyway + var desc = RuntimeInformation.FrameworkDescription; + if (string.IsNullOrWhiteSpace(desc)) + { + return DefaultJsonValue; + } + + // at this point, desc is non‑null, non‑empty (after trimming) + return desc.Trim(); + } + + /// + /// Truncates a string to the specified maximum length or returns a default value if input is null or empty. + /// + /// The string value to truncate + /// Maximum number of characters allowed + /// Truncated string or default value if input is invalid + internal static string TruncateOrDefault(string jsonStringVal, int maxChars) + { + try + { + if (string.IsNullOrEmpty(jsonStringVal)) + { + return DefaultJsonValue; + } + + if (jsonStringVal.Length <= maxChars) + { + return jsonStringVal; + } + + return jsonStringVal.Substring(0, maxChars); + } + catch + { + // Silently consume all exceptions + return DefaultJsonValue; + } + } + +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfoDto.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfoDto.cs new file mode 100644 index 0000000000..2c61d1c4bb --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfoDto.cs @@ -0,0 +1,53 @@ +// 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. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Data.Common; + +#nullable enable + +namespace Microsoft.Data.SqlClient.UserAgent; +internal class UserAgentInfoDto +{ + // Note: JSON key names are defined as constants to avoid reflection during serialization. + // This allows us to calculate their UTF-8 encoded byte sizes efficiently without instantiating + // the DTO or relying on JsonProperty attribute resolution at runtime. The small overhead of + // maintaining constants is justified by the performance and allocation savings. + + // Note: These values reflect the order of the JSON fields defined in the spec. + // The order is maintained to match the JSON payload structure. + public const string DriverJsonKey = "driver"; + public const string VersionJsonKey = "version"; + public const string OsJsonKey = "os"; + public const string ArchJsonKey = "arch"; + public const string RuntimeJsonKey = "runtime"; + + [JsonPropertyName(DriverJsonKey)] + public string Driver { get; set; } = string.Empty; + + [JsonPropertyName(VersionJsonKey)] + public string Version { get; set; } = string.Empty; + + [JsonPropertyName(OsJsonKey)] + public OsInfo? OS { get; set; } + + [JsonPropertyName(ArchJsonKey)] + public string? Arch { get; set; } + + [JsonPropertyName(RuntimeJsonKey)] + public string? Runtime { get; set; } + + public class OsInfo + { + public const string TypeJsonKey = "type"; + public const string DetailsJsonKey = "details"; + + [JsonPropertyName(TypeJsonKey)] + public string Type { get; set; } = string.Empty; + + [JsonPropertyName(DetailsJsonKey)] + public string? Details { get; set; } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs new file mode 100644 index 0000000000..fa5bf9752b --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs @@ -0,0 +1,333 @@ +// 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. + +using System; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Data.Common; +using Microsoft.Data.SqlClient.UserAgent; +using Xunit; + +#nullable enable + +namespace Microsoft.Data.SqlClient.UnitTests +{ + /// + /// Unit tests for and its companion DTO. + /// Focus areas: + /// 1. Cached payload size and non-nullability + /// 2. Default expected value check for payload fields + /// 3. Payload size adjustment and field dropping(all low priority fields) + /// 4. Payload size adjustment and field dropping(drop particular low priority fields: arch, runtime and os.description) + /// 5. DTO JSON contract (key names and values) + /// 6. Combined truncation, adjustment, and serialization + /// + public class UserAgentInfoTests + { + // Cached payload is within the 2,047‑byte spec and never null + [Fact] + public void CachedPayload_IsNotNull_And_WithinSpecLimit() + { + ReadOnlyMemory payload = UserAgentInfo.UserAgentCachedJsonPayload; + Assert.False(payload.IsEmpty); + Assert.InRange(payload.Length, 1, UserAgentInfo.JsonPayloadMaxBytes); + } + + // Cached payload contains the expected values for driver name and version + [Fact] + public void CachedPayload_Contains_Correct_DriverName_And_Version() + { + // Arrange: retrieve the raw JSON payload bytes and determine what we expect + ReadOnlyMemory payload = UserAgentInfo.UserAgentCachedJsonPayload; + Assert.False(payload.IsEmpty); // guard against empty payload + + // compute the expected driver and version + string expectedDriver = UserAgentInfo.DriverName; + string expectedVersion = ADP.GetAssemblyVersion().ToString(); + + // Act: turn the bytes back into JSON and pull out the fields + using JsonDocument document = JsonDocument.Parse(payload); + JsonElement root = document.RootElement; + string actualDriver = root.GetProperty(UserAgentInfoDto.DriverJsonKey).GetString()!; + string actualVersion = root.GetProperty(UserAgentInfoDto.VersionJsonKey).GetString()!; + + // Assert: the driver and version in the payload match the expected values + Assert.Equal(expectedDriver, actualDriver); + Assert.Equal(expectedVersion, actualVersion); + } + + // TruncateOrDefault respects null, empty, fit, and overflow cases + [Theory] + [InlineData(null, 5, "Unknown")] // null returns default + [InlineData("", 5, "Unknown")] // empty returns default + [InlineData("abc", 5, "abc")] // within limit unchanged + [InlineData("abcde", 5, "abcde")] // exact max chars + [InlineData("abcdef", 5, "abcde")] // overflow truncated + public void TruncateOrDefault_Behaviour(string? input, int max, string expected) + { + string actual = UserAgentInfo.TruncateOrDefault(input!, max); + Assert.Equal(expected, actual); + } + + // AdjustJsonPayloadSize drops all low‑priority fields when required + + /// + /// Verifies that AdjustJsonPayloadSize truncates the DTO’s JSON when it exceeds the maximum size. + /// High-priority fields (Driver, Version) must remain, low-priority fields (Arch, Runtime and OS) are removed + /// + [Fact] + public void AdjustJsonPayloadSize_DropAllLowPriorityFields_When_PayloadTooLarge() + { + // Arrange: create a DTO whose serialized JSON is guaranteed to exceed the max size + string huge = new string('x', 20_000); + var dto = new UserAgentInfoDto + { + Driver = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.DriverNameMaxChars), + Version = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.VersionMaxChars), + OS = new UserAgentInfoDto.OsInfo + { + Type = huge, + Details = huge + }, + Arch = huge, + Runtime = huge + }; + + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = null, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false + }; + + string expectedDriverName = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.DriverNameMaxChars); + string expectedVersion = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.VersionMaxChars); + + // Capture the size before the helper mutates the DTO + byte[] original = JsonSerializer.SerializeToUtf8Bytes(dto, options); + Assert.True(original.Length > UserAgentInfo.JsonPayloadMaxBytes); + + // Act: apply the size-adjustment helper + byte[] payload = UserAgentInfo.AdjustJsonPayloadSize(dto); + + // Assert: payload is smaller and not empty + Assert.NotEmpty(payload); + Assert.True(payload.Length < original.Length); + + // Structural checks using JsonDocument + using JsonDocument doc = JsonDocument.Parse(payload); + JsonElement root = doc.RootElement; + + // High-priority fields must still be present(driver name and version) + Assert.Equal(expectedDriverName, root.GetProperty(UserAgentInfoDto.DriverJsonKey).GetString()); + Assert.Equal(expectedVersion, root.GetProperty(UserAgentInfoDto.VersionJsonKey).GetString()); + + // Low-priority fields should have been removed(arch and runtime) + Assert.False(root.TryGetProperty(UserAgentInfoDto.ArchJsonKey, out _)); + Assert.False(root.TryGetProperty(UserAgentInfoDto.RuntimeJsonKey, out _)); + + // OS block should have been removed entirely + Assert.False(root.TryGetProperty(UserAgentInfoDto.OsJsonKey, out _)); + } + + /// + /// Verifies that AdjustJsonPayloadSize truncates the DTO’s JSON when it exceeds the maximum size. + /// High-priority fields (Driver, Version) must remain, low-priority fields (Arch, Runtime and OS.details) are removed + /// Note that OS subfield(Type) is preserved, but OS.Details is dropped. + /// + [Fact] + public void AdjustJsonPayloadSize_DropSpecificPriorityFields_Excluding_OsType_When_PayloadTooLarge() + { + // Arrange: create a DTO whose serialized JSON is guaranteed to exceed the max size + string huge = new string('x', 20_000); + var dto = new UserAgentInfoDto + { + Driver = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.DriverNameMaxChars), + Version = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.VersionMaxChars), + OS = new UserAgentInfoDto.OsInfo + { + Type = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.OsTypeMaxChars), + Details = huge + }, + Arch = huge, + Runtime = huge + }; + + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = null, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false + }; + + string expectedDriverName = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.DriverNameMaxChars); + string expectedVersion = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.VersionMaxChars); + string expectedOsType = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.OsTypeMaxChars); + + // Capture the size before the helper mutates the DTO + byte[] original = JsonSerializer.SerializeToUtf8Bytes(dto, options); + Assert.True(original.Length > UserAgentInfo.JsonPayloadMaxBytes); + + // Act: apply the size-adjustment helper + byte[] payload = UserAgentInfo.AdjustJsonPayloadSize(dto); + + // Assert: payload is smaller and not empty + Assert.NotEmpty(payload); + Assert.True(payload.Length < original.Length); + + // Structural checks using JsonDocument + using JsonDocument doc = JsonDocument.Parse(payload); + JsonElement root = doc.RootElement; + + // High-priority fields must still be present(driver name and version) and truncated to expected length + //Assert.True(root.TryGetProperty(UserAgentInfoDto.DriverJsonKey, out _)); + Assert.Equal(expectedDriverName, root.GetProperty(UserAgentInfoDto.DriverJsonKey).GetString()); + Assert.Equal(expectedVersion, root.GetProperty(UserAgentInfoDto.VersionJsonKey).GetString()); + //Assert.True(root.TryGetProperty(UserAgentInfoDto.VersionJsonKey, out _)); + + // Low-priority fields should have been removed(arch and runtime) + Assert.False(root.TryGetProperty(UserAgentInfoDto.ArchJsonKey, out _)); + Assert.False(root.TryGetProperty(UserAgentInfoDto.RuntimeJsonKey, out _)); + + Assert.True(root.TryGetProperty(UserAgentInfoDto.OsJsonKey, out JsonElement os)); + Assert.True(os.TryGetProperty(UserAgentInfoDto.OsInfo.TypeJsonKey, out JsonElement type)); + + Assert.Equal(expectedOsType, type.GetString()); + Assert.False(os.TryGetProperty(UserAgentInfoDto.OsInfo.DetailsJsonKey, out _)); + + } + + // DTO JSON contract - verify names and values(parameterized) + + /// + /// Verifies that UserAgentInfoDto serializes according to its JSON contract: + /// required fields always appear with correct values, optional fields + /// and the nested OS object are only emitted when non-null, + /// and all JSON property names match the defined constants. + /// + [Theory] + [InlineData("d", "v", "t", "dd", "a", "r")] + [InlineData("DeReaver", "1.2", "linux", "kernel", "", "")] + [InlineData("LongDrv", "2.0", "win", null, null, null)] + [InlineData("Driver", "Version", null, null, null, null)] // drop OsInfo entirely + public void Dto_JsonPropertyNames_MatchConstants( + string driver, + string version, + string? osType, + string? osDetails, + string? arch, + string? runtime) + { + // Arrange: build the DTO, dropping the OS object if osType is null + var dto = new UserAgentInfoDto + { + Driver = driver, + Version = version, + OS = osType == null + ? null + : new UserAgentInfoDto.OsInfo + { + Type = osType, + Details = string.IsNullOrEmpty(osDetails) ? null : osDetails + }, + Arch = string.IsNullOrEmpty(arch) ? null : arch, + Runtime = string.IsNullOrEmpty(runtime) ? null : runtime + }; + + // Arrange: configure JSON serialization to omit nulls and use exact property names + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = null, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false + }; + + // Act: serialize the DTO and parse it back into a JsonDocument + string json = JsonSerializer.Serialize(dto, options); + using var doc = JsonDocument.Parse(json); + JsonElement root = doc.RootElement; + + // Assert: required properties always present with correct values + Assert.Equal(driver, root.GetProperty(UserAgentInfoDto.DriverJsonKey).GetString()); + Assert.Equal(version, root.GetProperty(UserAgentInfoDto.VersionJsonKey).GetString()); + + // Assert: Arch is only present if non-null + if (dto.Arch == null) + { + Assert.False(root.TryGetProperty(UserAgentInfoDto.ArchJsonKey, out _)); + } + else + { + Assert.Equal(dto.Arch, root.GetProperty(UserAgentInfoDto.ArchJsonKey).GetString()); + } + + // Assert: Runtime is only present if non-null + if (dto.Runtime == null) + { + Assert.False(root.TryGetProperty(UserAgentInfoDto.RuntimeJsonKey, out _)); + } + else + { + Assert.Equal(dto.Runtime, root.GetProperty(UserAgentInfoDto.RuntimeJsonKey).GetString()); + } + + // Assert: OS object may be omitted entirely + if (dto.OS == null) + { + Assert.False(root.TryGetProperty(UserAgentInfoDto.OsJsonKey, out _)); + } + else + { + JsonElement os = root.GetProperty(UserAgentInfoDto.OsJsonKey); + + // OS.Type must always be present when OS is not null + Assert.Equal(dto.OS.Type, os.GetProperty(UserAgentInfoDto.OsInfo.TypeJsonKey).GetString()); + + // OS.Details is optional + if (dto.OS.Details == null) + { + Assert.False(os.TryGetProperty(UserAgentInfoDto.OsInfo.DetailsJsonKey, out _)); + } + else + { + Assert.Equal(dto.OS.Details, os.GetProperty(UserAgentInfoDto.OsInfo.DetailsJsonKey).GetString()); + } + } + } + + // End-to-end test that combines truncation, adjustment, and serialization + [Fact] + public void EndToEnd_Truncate_Adjust_Serialize_Works() + { + string raw = new string('x', 2_000); + const int Max = 100; + + string driver = UserAgentInfo.TruncateOrDefault(raw, Max); + string version = UserAgentInfo.TruncateOrDefault(raw, Max); + string osType = UserAgentInfo.TruncateOrDefault(raw, Max); + + var dto = new UserAgentInfoDto + { + Driver = driver, + Version = version, + OS = new UserAgentInfoDto.OsInfo { Type = osType, Details = raw }, + Arch = raw, + Runtime = raw + }; + + byte[] payload = UserAgentInfo.AdjustJsonPayloadSize(dto); + string json = Encoding.UTF8.GetString(payload); + + using JsonDocument doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + Assert.Equal(driver, root.GetProperty(UserAgentInfoDto.DriverJsonKey).GetString()); + Assert.Equal(version, root.GetProperty(UserAgentInfoDto.VersionJsonKey).GetString()); + + JsonElement os = root.GetProperty(UserAgentInfoDto.OsJsonKey); + Assert.Equal(osType, os.GetProperty(UserAgentInfoDto.OsInfo.TypeJsonKey).GetString()); + } + } +} From 13090ced0da89263b7f84b604f3ed0a57e3bbd5f Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Fri, 22 Aug 2025 16:25:46 -0700 Subject: [PATCH 02/19] Update truncation null checks --- .../Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs | 9 +++++++-- .../tests/UnitTests/UserAgentInfoTests.cs | 4 +--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs index d7aa1e1163..d927155dbb 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs @@ -330,7 +330,7 @@ private static string DetectRuntime() /// The string value to truncate /// Maximum number of characters allowed /// Truncated string or default value if input is invalid - internal static string TruncateOrDefault(string jsonStringVal, int maxChars) + internal static string TruncateOrDefault(string? jsonStringVal, int maxChars) { try { @@ -339,7 +339,12 @@ internal static string TruncateOrDefault(string jsonStringVal, int maxChars) return DefaultJsonValue; } - if (jsonStringVal.Length <= maxChars) + if (maxChars <= 0) + { + return DefaultJsonValue; + } + + if (jsonStringVal!.Length <= maxChars) { return jsonStringVal; } diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs index fa5bf9752b..d45c188c43 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs @@ -67,7 +67,7 @@ public void CachedPayload_Contains_Correct_DriverName_And_Version() [InlineData("abcdef", 5, "abcde")] // overflow truncated public void TruncateOrDefault_Behaviour(string? input, int max, string expected) { - string actual = UserAgentInfo.TruncateOrDefault(input!, max); + string actual = UserAgentInfo.TruncateOrDefault(input, max); Assert.Equal(expected, actual); } @@ -182,10 +182,8 @@ public void AdjustJsonPayloadSize_DropSpecificPriorityFields_Excluding_OsType_Wh JsonElement root = doc.RootElement; // High-priority fields must still be present(driver name and version) and truncated to expected length - //Assert.True(root.TryGetProperty(UserAgentInfoDto.DriverJsonKey, out _)); Assert.Equal(expectedDriverName, root.GetProperty(UserAgentInfoDto.DriverJsonKey).GetString()); Assert.Equal(expectedVersion, root.GetProperty(UserAgentInfoDto.VersionJsonKey).GetString()); - //Assert.True(root.TryGetProperty(UserAgentInfoDto.VersionJsonKey, out _)); // Low-priority fields should have been removed(arch and runtime) Assert.False(root.TryGetProperty(UserAgentInfoDto.ArchJsonKey, out _)); From c450c61fdc05d934f3fae69efd915365f1d770e3 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Tue, 9 Sep 2025 14:27:38 -0700 Subject: [PATCH 03/19] Enable UserAgent Feature Extension --- .../Data/SqlClient/SqlInternalConnectionTds.cs | 4 ++-- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 6 ++++++ .../Data/SqlClient/SqlInternalConnectionTds.cs | 2 +- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 18 +++++++++++++++++- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 9 ++++++++- 5 files changed, 34 insertions(+), 5 deletions(-) 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 1871fe6087..5afed84940 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 @@ -209,7 +209,7 @@ internal bool IsDNSCachingBeforeRedirectSupported internal bool IsJsonSupportEnabled = false; // User Agent Flag - internal bool IsUserAgentEnabled = true; + internal bool IsUserAgentSupportEnabled = true; // Vector Support Flag internal bool IsVectorSupportEnabled = false; @@ -1417,7 +1417,7 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, requestedFeatures |= TdsEnums.FeatureExtension.SQLDNSCaching; requestedFeatures |= TdsEnums.FeatureExtension.JsonSupport; requestedFeatures |= TdsEnums.FeatureExtension.VectorSupport; - + #if DEBUG requestedFeatures |= TdsEnums.FeatureExtension.UserAgent; #endif diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index 8fbeb3050c..bd27196923 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -8887,6 +8887,7 @@ private void WriteLoginData(SqlLogin rec, private int ApplyFeatureExData(TdsEnums.FeatureExtension requestedFeatures, SessionData recoverySessionData, FederatedAuthenticationFeatureExtensionData fedAuthFeatureExtensionData, + byte[] userAgentJsonPayload, bool useFeatureExt, int length, bool write = false) @@ -8939,6 +8940,11 @@ private int ApplyFeatureExData(TdsEnums.FeatureExtension requestedFeatures, length += WriteVectorSupportFeatureRequest(write); } + if ((requestedFeatures & TdsEnums.FeatureExtension.UserAgent) != 0) + { + length += WriteUserAgentFeatureRequest(userAgentJsonPayload, write); + } + length++; // for terminator if (write) { 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 2434582205..9fd0f59cb8 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 @@ -213,7 +213,7 @@ internal bool IsDNSCachingBeforeRedirectSupported internal bool IsVectorSupportEnabled = false; // User Agent Flag - internal bool IsUserAgentEnabled = true; + internal bool IsUserAgentSupportEnabled = true; // TCE flags internal byte _tceVersionSupported; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index d38190a359..4faea86d1b 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -29,6 +29,8 @@ using Microsoft.Data.SqlClient.DataClassification; using Microsoft.Data.SqlClient.LocalDb; using Microsoft.Data.SqlClient.Server; +using Microsoft.Data.SqlClient.UserAgent; + #if NETFRAMEWORK using Microsoft.Data.SqlTypes; #endif @@ -9035,7 +9037,15 @@ private void WriteLoginData(SqlLogin rec, } } - ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData, useFeatureExt, length, true); + ApplyFeatureExData( + requestedFeatures, + recoverySessionData, + fedAuthFeatureExtensionData, + UserAgentInfo.UserAgentCachedJsonPayload.ToArray(), + useFeatureExt, + length, + true + ); } catch (Exception e) { @@ -9054,6 +9064,7 @@ private void WriteLoginData(SqlLogin rec, private int ApplyFeatureExData(TdsEnums.FeatureExtension requestedFeatures, SessionData recoverySessionData, FederatedAuthenticationFeatureExtensionData fedAuthFeatureExtensionData, + byte[] userAgentJsonPayload, bool useFeatureExt, int length, bool write = false) @@ -9108,6 +9119,11 @@ private int ApplyFeatureExData(TdsEnums.FeatureExtension requestedFeatures, length += WriteVectorSupportFeatureRequest(write); } + if ((requestedFeatures & TdsEnums.FeatureExtension.UserAgent) != 0) + { + length += WriteUserAgentFeatureRequest(userAgentJsonPayload, write); + } + length++; // for terminator if (write) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index 6226f958a5..f039e50752 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -2,6 +2,7 @@ using System.Buffers; using System.Diagnostics; using System.Text; +using Microsoft.Data.SqlClient.UserAgent; using Microsoft.Data.SqlClient.Utilities; #nullable enable @@ -192,7 +193,13 @@ internal void TdsLogin( int feOffset = length; // calculate and reserve the required bytes for the featureEx - length = ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData, useFeatureExt, length); + length = ApplyFeatureExData(requestedFeatures, + recoverySessionData, + fedAuthFeatureExtensionData, + UserAgentInfo.UserAgentCachedJsonPayload.ToArray(), + useFeatureExt, + length + ); WriteLoginData(rec, requestedFeatures, From c8897d34dd8409628a9932c2a8ff0aa87a82ec72 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Tue, 9 Sep 2025 17:07:53 -0700 Subject: [PATCH 04/19] Add new functional tests for UserAgent FE --- .../SqlClient/SqlInternalConnectionTds.cs | 6 + .../src/Microsoft/Data/SqlClient/TdsParser.cs | 12 +- .../SqlClient/SqlInternalConnectionTds.cs | 7 +- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 3 +- .../SqlConnectionBasicTests.cs | 111 ++++++++++++++++++ .../TDS/TDS.EndPoint/ITDSServerSession.cs | 5 + .../tools/TDS/TDS.Servers/GenericTDSServer.cs | 59 +++++++++- .../TDS.Servers/GenericTDSServerSession.cs | 5 + .../tests/tools/TDS/TDS/TDSFeatureID.cs | 5 + 9 files changed, 209 insertions(+), 4 deletions(-) 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 5afed84940..533e9d70db 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 @@ -3000,6 +3000,12 @@ internal void OnFeatureExtAck(int featureId, byte[] data) IsVectorSupportEnabled = true; break; } + case TdsEnums.FEATUREEXT_USERAGENT: + { + // Unexpected ack from server but we ignore it entirely + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for USERAGENTSUPPORT (ignored)", ObjectID); + break; + } default: { diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index bd27196923..6e3af2a0a2 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -29,6 +29,8 @@ using Microsoft.Data.SqlClient.DataClassification; using Microsoft.Data.SqlClient.LocalDb; using Microsoft.Data.SqlClient.Server; +using Microsoft.Data.SqlClient.UserAgent; + #if NETFRAMEWORK using Microsoft.Data.SqlTypes; #endif @@ -8868,7 +8870,15 @@ private void WriteLoginData(SqlLogin rec, } } - ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData, useFeatureExt, length, true); + ApplyFeatureExData( + requestedFeatures, + recoverySessionData, + fedAuthFeatureExtensionData, + UserAgentInfo.UserAgentCachedJsonPayload.ToArray(), + useFeatureExt, + length, + true + ); } catch (Exception e) { 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 9fd0f59cb8..032b2ba3e2 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 @@ -3038,7 +3038,12 @@ internal void OnFeatureExtAck(int featureId, byte[] data) IsVectorSupportEnabled = true; break; } - + case TdsEnums.FEATUREEXT_USERAGENT: + { + // Unexpected ack from server but we ignore it entirely + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for USERAGENTSUPPORT (ignored)", ObjectID); + break; + } default: { // Unknown feature ack diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index f039e50752..5f47011209 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -193,7 +193,8 @@ internal void TdsLogin( int feOffset = length; // calculate and reserve the required bytes for the featureEx - length = ApplyFeatureExData(requestedFeatures, + length = ApplyFeatureExData( + requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData, UserAgentInfo.UserAgentCachedJsonPayload.ToArray(), diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs index 616a8fec6f..13edff2b46 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs @@ -620,5 +620,116 @@ public void TestConnWithVectorFeatExtVersionNegotiation(bool expectedConnectionR Assert.Throws(() => connection.Open()); } } + + // Test to verify client sends a UserAgent version + // We do not receive any Ack for it from the server + [Fact] + public void TestConnWithUnackedUserAgentFeatureExtension() + { + using var server = TestTdsServer.StartTestServer(); + + // Configure the server to support UserAgent version 0x01 + server.ServerSupportedUserAgentFeatureExtVersion = 0x01; + server.EnableUserAgentFeatureExt = true; + // By design its response logic never emits an ACK + bool loginFound = false; + bool responseFound = false; + + // Inspect what the client sends in the LOGIN7 packet + server.OnLogin7Validated = loginToken => + { + var token = loginToken.FeatureExt + .OfType() + .FirstOrDefault(t => t.FeatureID == TDSFeatureID.UserAgentSupport); + if (token != null) + { + Assert.Equal((byte)TDSFeatureID.UserAgentSupport, (byte)token.FeatureID); + Assert.Equal(0x1, token.Data[0]); + loginFound = true; + } + }; + + // Inspect whether the server ever sends back an ACK + server.OnAuthenticationResponseCompleted = response => + { + var ack = response + .OfType() + .SelectMany(t => t.Options) + .OfType() + .FirstOrDefault(o => o.FeatureID == TDSFeatureID.UserAgentSupport); + if (ack != null) + { + responseFound = true; + } + }; + + // Open the connection (this triggers the LOGIN7 exchange) + using var connection = new SqlConnection(server.ConnectionString); + connection.Open(); + + // Verify client did offer UserAgent + Assert.True(loginFound, "Expected UserAgent extension in LOGIN7"); + + // Verify server never acknowledged it + Assert.False(responseFound, "Server should not acknowledge UserAgent"); + + // Verify the connection itself succeeded + Assert.Equal(ConnectionState.Open, connection.State); + } + + // Test to verify the driver behaviour even if server sends an Ack + [Fact] + public void TestConnWithAckedUserAgentFeatureExtension() + { + using var server = TestTdsServer.StartTestServer(); + + // Configure the test server + server.ServerSupportedUserAgentFeatureExtVersion = 0x01; + server.EnableUserAgentFeatureExt = true; + + // Opt in to forced ACK for UserAgentSupport (no negotiation) + server.EmitUserAgentFeatureExtAck = true; + + bool loginFound = false; + bool responseFound = false; + + // Observe what the client sends in LOGIN7 + server.OnLogin7Validated = loginToken => + { + var token = loginToken.FeatureExt + .OfType() + .FirstOrDefault(t => t.FeatureID == TDSFeatureID.UserAgentSupport); + if (token != null) + { + Assert.Equal((byte)TDSFeatureID.UserAgentSupport, (byte)token.FeatureID); + loginFound = true; + } + }; + + // Verify the server sent back an ACK for UserAgentSupport + server.OnAuthenticationResponseCompleted = response => + { + // Find any FeatureExtAck option with FeatureID == UserAgentSupport + var uaAckOptions = response + .OfType() + .SelectMany(t => t.Options) + .OfType() + .Where(o => o.FeatureID == TDSFeatureID.UserAgentSupport) + .ToList(); + + Assert.True(uaAckOptions.Count >= 1, "Expected an ACK for UserAgentSupport"); + responseFound = true; + }; + + // Act: open the connection which triggers the LOGIN7 exchange + using var connection = new SqlConnection(server.ConnectionString); + connection.Open(); + + // Assert: client advertised the feature, server acknowledged it, connection is healthy + Assert.True(loginFound, "Expected UserAgent extension in LOGIN7"); + Assert.True(responseFound, "Server should acknowledge UserAgent when forced"); + Assert.Equal(ConnectionState.Open, connection.State); + } + } } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs index 9b5b7804b4..b511cbc8d2 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs @@ -93,5 +93,10 @@ public interface ITDSServerSession /// Indicates whether the client supports Vector column type /// bool IsVectorSupportEnabled { get; set; } + + /// + /// Indicates whether the client supports UserAgent Feature Extension + /// + bool IsUserAgentSupportEnabled { get; set; } } } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServer.cs index ac04fd2f57..e31e09f3c6 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServer.cs @@ -54,16 +54,41 @@ public delegate void OnAuthenticationCompletedDelegate( /// public bool EnableVectorFeatureExt { get; set; } = false; + /// + /// Property for enabling user agent feature extension. + /// + public bool EnableUserAgentFeatureExt { get; set; } = true; + /// /// Property for setting server version for vector feature extension. /// public byte ServerSupportedVectorFeatureExtVersion { get; set; } = DefaultSupportedVectorFeatureExtVersion; + /// + /// Property for setting server version for user agent feature extension. + /// + public byte ServerSupportedUserAgentFeatureExtVersion { get; set; } = DefaultSupportedUserAgentFeatureExtVersion; + /// /// Client version for vector FeatureExtension. /// private byte _clientSupportedVectorFeatureExtVersion = 0; + /// + /// Client version for User Agent FeatureExtension. + /// + private byte _clientSupportedUserAgentFeatureExtVersion = 0; + + /// + /// Server will ACK UserAgentSupport in the login response when this property is set to true. + /// + public bool EmitUserAgentFeatureExtAck { get; set; } = false; + + /// + /// Default feature extension version supported on the server for user agent. + /// + public const byte DefaultSupportedUserAgentFeatureExtVersion = 0x01; + /// /// Session counter /// @@ -287,7 +312,14 @@ public virtual TDSMessageCollection OnLogin7Request(ITDSServerSession session, T } break; } - + case TDSFeatureID.UserAgentSupport: + { + if (EnableUserAgentFeatureExt) + { + _clientSupportedUserAgentFeatureExtVersion = ((TDSLogin7GenericOptionToken)option).Data[0]; + } + break; + } default: { // Do nothing @@ -654,6 +686,31 @@ protected virtual TDSMessageCollection OnAuthenticationCompleted(ITDSServerSessi } } + // If tests request it, force an ACK for UserAgentSupport with no negotiation + if (EmitUserAgentFeatureExtAck) + { + byte ackVersion = ServerSupportedUserAgentFeatureExtVersion; + + var data = new byte[] { ackVersion }; + var uaAck = new TDSFeatureExtAckGenericOption( + TDSFeatureID.UserAgentSupport, + (uint)data.Length, + data); + + // Reuse an existing FeatureExtAck token if present, otherwise add a new one + var featureExtAckToken = responseMessage.OfType().FirstOrDefault(); + if (featureExtAckToken == null) + { + featureExtAckToken = new TDSFeatureExtAckToken(uaAck); + responseMessage.Add(featureExtAckToken); + } + else + { + featureExtAckToken.Options.Add(uaAck); + } + } + + // Create DONE token TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final); diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServerSession.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServerSession.cs index e9e65d5f8f..986b27a4dd 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServerSession.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServerSession.cs @@ -124,6 +124,11 @@ public class GenericTDSServerSession : ITDSServerSession /// public bool IsVectorSupportEnabled { get; set; } + /// + /// Indicates whether this session supports User Agent Feature Extension + /// + public bool IsUserAgentSupportEnabled { get; set; } + #region Session Options /// diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDSFeatureID.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDSFeatureID.cs index 6bb6fbc8d2..258ad7e1f3 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDSFeatureID.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDSFeatureID.cs @@ -29,6 +29,11 @@ public enum TDSFeatureID : byte /// VectorSupport = 0x0E, + /// + /// User Agent Support + /// + UserAgentSupport = 0x0F, + /// /// End of the list /// From c0eea2bf55fa9f3a4d39f4ff71119cd974435990 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Wed, 10 Sep 2025 19:39:47 -0700 Subject: [PATCH 05/19] Update functional test to verify driver behaviour --- .../SqlClient/SqlInternalConnectionTds.cs | 6 - .../SqlClient/SqlInternalConnectionTds.cs | 5 +- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 6 + ...soft.Data.SqlClient.FunctionalTests.csproj | 2 + .../SqlConnectionBasicTests.cs | 125 +++++++++--------- 5 files changed, 71 insertions(+), 73 deletions(-) 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 533e9d70db..ad1d18fd49 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 @@ -208,9 +208,6 @@ internal bool IsDNSCachingBeforeRedirectSupported // Json Support Flag internal bool IsJsonSupportEnabled = false; - // User Agent Flag - internal bool IsUserAgentSupportEnabled = true; - // Vector Support Flag internal bool IsVectorSupportEnabled = false; @@ -1417,10 +1414,7 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, requestedFeatures |= TdsEnums.FeatureExtension.SQLDNSCaching; requestedFeatures |= TdsEnums.FeatureExtension.JsonSupport; requestedFeatures |= TdsEnums.FeatureExtension.VectorSupport; - - #if DEBUG requestedFeatures |= TdsEnums.FeatureExtension.UserAgent; - #endif _parser.TdsLogin(login, requestedFeatures, _recoverySessionData, _fedAuthFeatureExtensionData, encrypt); } 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 032b2ba3e2..7dbb6e07de 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 @@ -212,9 +212,6 @@ internal bool IsDNSCachingBeforeRedirectSupported // Vector Support Flag internal bool IsVectorSupportEnabled = false; - // User Agent Flag - internal bool IsUserAgentSupportEnabled = true; - // TCE flags internal byte _tceVersionSupported; @@ -3040,7 +3037,9 @@ internal void OnFeatureExtAck(int featureId, byte[] data) } case TdsEnums.FEATUREEXT_USERAGENT: { + // TODO: define comment, TDS spec doesnot define an ack // Unexpected ack from server but we ignore it entirely + // TODO for tfuture if we can find and verify this log message SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for USERAGENTSUPPORT (ignored)", ObjectID); break; } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index 5f47011209..0bcb462d1a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -192,6 +192,12 @@ internal void TdsLogin( } int feOffset = length; + + // NOTE: This approach of pre-calculating the packet length is inefficient. + // We're making 2 passes over the data to be written. + // Instead, we should be writing everything to the buffer once, + // leaving a hole where the header length goes. + // calculate and reserve the required bytes for the featureEx length = ApplyFeatureExData( requestedFeatures, diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj index 91a5a505b9..1d9e28f7bb 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj @@ -92,6 +92,8 @@ + + diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs index 13edff2b46..4b51823b06 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs @@ -9,14 +9,12 @@ using System.Globalization; using System.Linq; using System.Reflection; -using System.Runtime.InteropServices; using System.Security; using System.Threading; using System.Threading.Tasks; using Microsoft.SqlServer.TDS; using Microsoft.SqlServer.TDS.FeatureExtAck; using Microsoft.SqlServer.TDS.Login7; -using Microsoft.SqlServer.TDS.PreLogin; using Microsoft.SqlServer.TDS.Servers; using Xunit; @@ -621,30 +619,46 @@ public void TestConnWithVectorFeatExtVersionNegotiation(bool expectedConnectionR } } - // Test to verify client sends a UserAgent version - // We do not receive any Ack for it from the server - [Fact] - public void TestConnWithUnackedUserAgentFeatureExtension() + // Test to verify client sends a UserAgent version + // and driver behaviour if server sends an Ack or not + [Theory] + [InlineData(false, false)] // We do not receive any Ack from the server + [InlineData(true, true)] // Server sends an Ack + public void TestConnWithUserAgentFeatureExtension(bool forceAck, bool expectAck) { using var server = TestTdsServer.StartTestServer(); // Configure the server to support UserAgent version 0x01 server.ServerSupportedUserAgentFeatureExtVersion = 0x01; server.EnableUserAgentFeatureExt = true; - // By design its response logic never emits an ACK + + // Opt in to forced ACK for UserAgentSupport (no negotiation) + server.EmitUserAgentFeatureExtAck = forceAck; + bool loginFound = false; bool responseFound = false; + // Captured from LOGIN7 as parsed by the test server + byte observedVersion = 0; + byte[] observedJsonBytes = Array.Empty(); + // Inspect what the client sends in the LOGIN7 packet server.OnLogin7Validated = loginToken => { var token = loginToken.FeatureExt - .OfType() - .FirstOrDefault(t => t.FeatureID == TDSFeatureID.UserAgentSupport); + .OfType() + .FirstOrDefault(t => t.FeatureID == TDSFeatureID.UserAgentSupport); + if (token != null) { Assert.Equal((byte)TDSFeatureID.UserAgentSupport, (byte)token.FeatureID); - Assert.Equal(0x1, token.Data[0]); + + // Layout: [0] = version byte, rest = UTF-8 JSON blob + Assert.True(token.Data.Length >= 2, "UserAgent token is too short"); + observedVersion = token.Data[0]; + Assert.Equal(0x1, observedVersion); + + observedJsonBytes = token.Data.AsSpan(1).ToArray(); loginFound = true; } }; @@ -652,84 +666,67 @@ public void TestConnWithUnackedUserAgentFeatureExtension() // Inspect whether the server ever sends back an ACK server.OnAuthenticationResponseCompleted = response => { - var ack = response + var uaAckOptions = response .OfType() .SelectMany(t => t.Options) .OfType() - .FirstOrDefault(o => o.FeatureID == TDSFeatureID.UserAgentSupport); - if (ack != null) + .Where(o => o.FeatureID == TDSFeatureID.UserAgentSupport) + .ToList(); + + if (uaAckOptions.Count > 0) { responseFound = true; } + + if (expectAck) + { + Assert.True(uaAckOptions.Count >= 1, "Expected an ACK for UserAgentSupport"); + } }; - // Open the connection (this triggers the LOGIN7 exchange) using var connection = new SqlConnection(server.ConnectionString); connection.Open(); // Verify client did offer UserAgent Assert.True(loginFound, "Expected UserAgent extension in LOGIN7"); - // Verify server never acknowledged it - Assert.False(responseFound, "Server should not acknowledge UserAgent"); + // Verify server ACK presence or absence per scenario + if (expectAck) + { + Assert.True(responseFound, "Server should acknowledge UserAgent when forced"); + } + else + { + Assert.False(responseFound, "Server should not acknowledge UserAgent"); + } // Verify the connection itself succeeded Assert.Equal(ConnectionState.Open, connection.State); - } - // Test to verify the driver behaviour even if server sends an Ack - [Fact] - public void TestConnWithAckedUserAgentFeatureExtension() - { - using var server = TestTdsServer.StartTestServer(); + // Note: Accessing UserAgentInfo via Reflection. + // We cannot use InternalsVisibleTo here because making internals visible to FunctionalTests + // causes the *.TestHarness.cs stubs to clash with the real internal types in SqlClient. + var asm = typeof(SqlConnection).Assembly; + var userAgentInfoType = + asm.GetTypes().FirstOrDefault(t => string.Equals(t.Name, "UserAgentInfo", StringComparison.Ordinal)) ?? + asm.GetTypes().FirstOrDefault(t => t.FullName?.EndsWith(".UserAgentInfo", StringComparison.Ordinal) == true); - // Configure the test server - server.ServerSupportedUserAgentFeatureExtVersion = 0x01; - server.EnableUserAgentFeatureExt = true; + Assert.True(userAgentInfoType != null, + $"Unable to find UserAgentInfo type in assembly {asm.FullName}"); - // Opt in to forced ACK for UserAgentSupport (no negotiation) - server.EmitUserAgentFeatureExtAck = true; + // Try to get the property + var prop = userAgentInfoType.GetProperty("UserAgentCachedJsonPayload", + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); - bool loginFound = false; - bool responseFound = false; + Assert.True(prop != null, + "Unable to find property 'UserAgentCachedJsonPayload' on UserAgentInfo"); - // Observe what the client sends in LOGIN7 - server.OnLogin7Validated = loginToken => - { - var token = loginToken.FeatureExt - .OfType() - .FirstOrDefault(t => t.FeatureID == TDSFeatureID.UserAgentSupport); - if (token != null) - { - Assert.Equal((byte)TDSFeatureID.UserAgentSupport, (byte)token.FeatureID); - loginFound = true; - } - }; + ReadOnlyMemory cachedPayload = (ReadOnlyMemory)prop.GetValue(null)!; - // Verify the server sent back an ACK for UserAgentSupport - server.OnAuthenticationResponseCompleted = response => - { - // Find any FeatureExtAck option with FeatureID == UserAgentSupport - var uaAckOptions = response - .OfType() - .SelectMany(t => t.Options) - .OfType() - .Where(o => o.FeatureID == TDSFeatureID.UserAgentSupport) - .ToList(); - - Assert.True(uaAckOptions.Count >= 1, "Expected an ACK for UserAgentSupport"); - responseFound = true; - }; - - // Act: open the connection which triggers the LOGIN7 exchange - using var connection = new SqlConnection(server.ConnectionString); - connection.Open(); + Assert.False(cachedPayload.IsEmpty); + Assert.True(observedJsonBytes.AsSpan().SequenceEqual(cachedPayload.Span), + "Observed UserAgent JSON does not match the cached payload bytes"); - // Assert: client advertised the feature, server acknowledged it, connection is healthy - Assert.True(loginFound, "Expected UserAgent extension in LOGIN7"); - Assert.True(responseFound, "Server should acknowledge UserAgent when forced"); - Assert.Equal(ConnectionState.Open, connection.State); } - } } From 26dd6f9f6ef062c037a11ba3d37111ab4b63b178 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Wed, 10 Sep 2025 19:46:41 -0700 Subject: [PATCH 06/19] Assertion update --- .../SqlConnectionBasicTests.cs | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs index 4b51823b06..68c104d24f 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs @@ -648,19 +648,21 @@ public void TestConnWithUserAgentFeatureExtension(bool forceAck, bool expectAck) var token = loginToken.FeatureExt .OfType() .FirstOrDefault(t => t.FeatureID == TDSFeatureID.UserAgentSupport); + - if (token != null) - { - Assert.Equal((byte)TDSFeatureID.UserAgentSupport, (byte)token.FeatureID); + // Test should fail if no UserAgent FE token is found + Assert.NotNull(token); - // Layout: [0] = version byte, rest = UTF-8 JSON blob - Assert.True(token.Data.Length >= 2, "UserAgent token is too short"); - observedVersion = token.Data[0]; - Assert.Equal(0x1, observedVersion); + Assert.Equal((byte)TDSFeatureID.UserAgentSupport, (byte)token.FeatureID); - observedJsonBytes = token.Data.AsSpan(1).ToArray(); - loginFound = true; - } + // Layout: [0] = version byte, rest = UTF-8 JSON blob + Assert.True(token.Data.Length >= 2, "UserAgent token is too short"); + + observedVersion = token.Data[0]; + Assert.Equal(0x1, observedVersion); + + observedJsonBytes = token.Data.AsSpan(1).ToArray(); + loginFound = true; }; // Inspect whether the server ever sends back an ACK From 8ad3c699d51506589a0fcb2ae1a22f920e04ffa2 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Wed, 10 Sep 2025 19:57:58 -0700 Subject: [PATCH 07/19] Remove unused flags and conditionals --- .../tests/FunctionalTests/SqlConnectionBasicTests.cs | 2 +- .../tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs | 5 ----- .../tests/tools/TDS/TDS.Servers/GenericTDSServer.cs | 10 +--------- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs index 68c104d24f..7f23835e42 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs @@ -630,7 +630,6 @@ public void TestConnWithUserAgentFeatureExtension(bool forceAck, bool expectAck) // Configure the server to support UserAgent version 0x01 server.ServerSupportedUserAgentFeatureExtVersion = 0x01; - server.EnableUserAgentFeatureExt = true; // Opt in to forced ACK for UserAgentSupport (no negotiation) server.EmitUserAgentFeatureExtAck = forceAck; @@ -665,6 +664,7 @@ public void TestConnWithUserAgentFeatureExtension(bool forceAck, bool expectAck) loginFound = true; }; + // TODO: Confirm the server sent an Ack by reading log message from SqlInternalConnectionTds // Inspect whether the server ever sends back an ACK server.OnAuthenticationResponseCompleted = response => { diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs index b511cbc8d2..9b5b7804b4 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs @@ -93,10 +93,5 @@ public interface ITDSServerSession /// Indicates whether the client supports Vector column type /// bool IsVectorSupportEnabled { get; set; } - - /// - /// Indicates whether the client supports UserAgent Feature Extension - /// - bool IsUserAgentSupportEnabled { get; set; } } } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServer.cs index e31e09f3c6..0db0c13d64 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServer.cs @@ -54,11 +54,6 @@ public delegate void OnAuthenticationCompletedDelegate( /// public bool EnableVectorFeatureExt { get; set; } = false; - /// - /// Property for enabling user agent feature extension. - /// - public bool EnableUserAgentFeatureExt { get; set; } = true; - /// /// Property for setting server version for vector feature extension. /// @@ -314,10 +309,7 @@ public virtual TDSMessageCollection OnLogin7Request(ITDSServerSession session, T } case TDSFeatureID.UserAgentSupport: { - if (EnableUserAgentFeatureExt) - { - _clientSupportedUserAgentFeatureExtVersion = ((TDSLogin7GenericOptionToken)option).Data[0]; - } + _clientSupportedUserAgentFeatureExtVersion = ((TDSLogin7GenericOptionToken)option).Data[0]; break; } default: From 21840359e2b7c55c64929100df6e3b29909e74c2 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Wed, 10 Sep 2025 20:05:18 -0700 Subject: [PATCH 08/19] Remove IsUserAgentSupportEnabled flag --- .../tests/tools/TDS/TDS.Servers/GenericTDSServerSession.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServerSession.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServerSession.cs index 986b27a4dd..e9e65d5f8f 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServerSession.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServerSession.cs @@ -124,11 +124,6 @@ public class GenericTDSServerSession : ITDSServerSession /// public bool IsVectorSupportEnabled { get; set; } - /// - /// Indicates whether this session supports User Agent Feature Extension - /// - public bool IsUserAgentSupportEnabled { get; set; } - #region Session Options /// From 3b51844234e51ae84dc31d9e5bc2498bef664934 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Thu, 11 Sep 2025 16:41:06 -0700 Subject: [PATCH 09/19] Test cleanup and identifier update --- .../SqlClient/SqlInternalConnectionTds.cs | 10 +++- .../src/Microsoft/Data/SqlClient/TdsEnums.cs | 2 +- ...soft.Data.SqlClient.FunctionalTests.csproj | 2 - .../SqlConnectionBasicTests.cs | 57 +++---------------- .../tools/TDS/TDS.Servers/GenericTDSServer.cs | 12 +--- .../tests/tools/TDS/TDS/TDSFeatureID.cs | 2 +- 6 files changed, 19 insertions(+), 66 deletions(-) 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 7dbb6e07de..916f7fe25d 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 @@ -3037,9 +3037,13 @@ internal void OnFeatureExtAck(int featureId, byte[] data) } case TdsEnums.FEATUREEXT_USERAGENT: { - // TODO: define comment, TDS spec doesnot define an ack - // Unexpected ack from server but we ignore it entirely - // TODO for tfuture if we can find and verify this log message + // TODO: Verify that the server sends an acknowledgment (Ack) + // using this log message in the future. + + // This Ack from the server is unexpected and is ignored completely. + // According to the TDS specification, an Ack is not defined/expected + // for this scenario. We handle it only for completeness + // and to support testing. SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for USERAGENTSUPPORT (ignored)", ObjectID); break; } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs index 3113e19625..5101c51649 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -242,7 +242,7 @@ public enum EnvChangeType : byte public const byte FEATUREEXT_JSONSUPPORT = 0x0D; public const byte FEATUREEXT_VECTORSUPPORT = 0x0E; // TODO: re-verify if this byte competes with another feature - public const byte FEATUREEXT_USERAGENT = 0x0F; + public const byte FEATUREEXT_USERAGENT = 0x10; [Flags] public enum FeatureExtension : uint diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj index 1d9e28f7bb..91a5a505b9 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj @@ -92,8 +92,6 @@ - - diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs index 7f23835e42..95d9c6de2a 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs @@ -619,12 +619,12 @@ public void TestConnWithVectorFeatExtVersionNegotiation(bool expectedConnectionR } } - // Test to verify client sends a UserAgent version - // and driver behaviour if server sends an Ack or not + // Test to verify that the client sends a UserAgent version + // and driver behaves correctly even if server sent an Ack [Theory] - [InlineData(false, false)] // We do not receive any Ack from the server - [InlineData(true, true)] // Server sends an Ack - public void TestConnWithUserAgentFeatureExtension(bool forceAck, bool expectAck) + [InlineData(false)] // We do not force test server to send an Ack + [InlineData(true)] // Server is forced to send an Ack + public void TestConnWithUserAgentFeatureExtension(bool forceAck) { using var server = TestTdsServer.StartTestServer(); @@ -635,7 +635,6 @@ public void TestConnWithUserAgentFeatureExtension(bool forceAck, bool expectAck) server.EmitUserAgentFeatureExtAck = forceAck; bool loginFound = false; - bool responseFound = false; // Captured from LOGIN7 as parsed by the test server byte observedVersion = 0; @@ -665,43 +664,12 @@ public void TestConnWithUserAgentFeatureExtension(bool forceAck, bool expectAck) }; // TODO: Confirm the server sent an Ack by reading log message from SqlInternalConnectionTds - // Inspect whether the server ever sends back an ACK - server.OnAuthenticationResponseCompleted = response => - { - var uaAckOptions = response - .OfType() - .SelectMany(t => t.Options) - .OfType() - .Where(o => o.FeatureID == TDSFeatureID.UserAgentSupport) - .ToList(); - - if (uaAckOptions.Count > 0) - { - responseFound = true; - } - - if (expectAck) - { - Assert.True(uaAckOptions.Count >= 1, "Expected an ACK for UserAgentSupport"); - } - }; - using var connection = new SqlConnection(server.ConnectionString); connection.Open(); // Verify client did offer UserAgent Assert.True(loginFound, "Expected UserAgent extension in LOGIN7"); - // Verify server ACK presence or absence per scenario - if (expectAck) - { - Assert.True(responseFound, "Server should acknowledge UserAgent when forced"); - } - else - { - Assert.False(responseFound, "Server should not acknowledge UserAgent"); - } - // Verify the connection itself succeeded Assert.Equal(ConnectionState.Open, connection.State); @@ -713,22 +681,15 @@ public void TestConnWithUserAgentFeatureExtension(bool forceAck, bool expectAck) asm.GetTypes().FirstOrDefault(t => string.Equals(t.Name, "UserAgentInfo", StringComparison.Ordinal)) ?? asm.GetTypes().FirstOrDefault(t => t.FullName?.EndsWith(".UserAgentInfo", StringComparison.Ordinal) == true); - Assert.True(userAgentInfoType != null, - $"Unable to find UserAgentInfo type in assembly {asm.FullName}"); - + Assert.NotNull(userAgentInfoType); + // Try to get the property var prop = userAgentInfoType.GetProperty("UserAgentCachedJsonPayload", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); - - Assert.True(prop != null, - "Unable to find property 'UserAgentCachedJsonPayload' on UserAgentInfo"); + Assert.NotNull(prop); ReadOnlyMemory cachedPayload = (ReadOnlyMemory)prop.GetValue(null)!; - - Assert.False(cachedPayload.IsEmpty); - Assert.True(observedJsonBytes.AsSpan().SequenceEqual(cachedPayload.Span), - "Observed UserAgent JSON does not match the cached payload bytes"); - + Assert.Equal(cachedPayload.ToArray(), observedJsonBytes.ToArray()); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServer.cs index 0db0c13d64..1fdb90d608 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServer.cs @@ -69,11 +69,6 @@ public delegate void OnAuthenticationCompletedDelegate( /// private byte _clientSupportedVectorFeatureExtVersion = 0; - /// - /// Client version for User Agent FeatureExtension. - /// - private byte _clientSupportedUserAgentFeatureExtVersion = 0; - /// /// Server will ACK UserAgentSupport in the login response when this property is set to true. /// @@ -307,11 +302,7 @@ public virtual TDSMessageCollection OnLogin7Request(ITDSServerSession session, T } break; } - case TDSFeatureID.UserAgentSupport: - { - _clientSupportedUserAgentFeatureExtVersion = ((TDSLogin7GenericOptionToken)option).Data[0]; - break; - } + default: { // Do nothing @@ -702,7 +693,6 @@ protected virtual TDSMessageCollection OnAuthenticationCompleted(ITDSServerSessi } } - // Create DONE token TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final); diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDSFeatureID.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDSFeatureID.cs index 258ad7e1f3..7681b72ac1 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDSFeatureID.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDSFeatureID.cs @@ -32,7 +32,7 @@ public enum TDSFeatureID : byte /// /// User Agent Support /// - UserAgentSupport = 0x0F, + UserAgentSupport = 0x10, /// /// End of the list From d67310474205dcbdd8a645887c22191bf7d6b5dd Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Mon, 6 Oct 2025 17:10:48 -0700 Subject: [PATCH 10/19] Fix server side throw issue --- .../SqlConnectionBasicTests.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs index 95d9c6de2a..66b041ac35 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs @@ -646,20 +646,19 @@ public void TestConnWithUserAgentFeatureExtension(bool forceAck) var token = loginToken.FeatureExt .OfType() .FirstOrDefault(t => t.FeatureID == TDSFeatureID.UserAgentSupport); - - - // Test should fail if no UserAgent FE token is found - Assert.NotNull(token); - - Assert.Equal((byte)TDSFeatureID.UserAgentSupport, (byte)token.FeatureID); + if (token == null) + { + return; + } - // Layout: [0] = version byte, rest = UTF-8 JSON blob - Assert.True(token.Data.Length >= 2, "UserAgent token is too short"); - - observedVersion = token.Data[0]; - Assert.Equal(0x1, observedVersion); + var data = token.Data; + if (data == null || data.Length < 2) + { + return; + } - observedJsonBytes = token.Data.AsSpan(1).ToArray(); + observedVersion = data[0]; + observedJsonBytes = data.AsSpan(1).ToArray(); loginFound = true; }; @@ -667,12 +666,13 @@ public void TestConnWithUserAgentFeatureExtension(bool forceAck) using var connection = new SqlConnection(server.ConnectionString); connection.Open(); - // Verify client did offer UserAgent - Assert.True(loginFound, "Expected UserAgent extension in LOGIN7"); - // Verify the connection itself succeeded Assert.Equal(ConnectionState.Open, connection.State); + // Verify client did offer UserAgent + Assert.True(loginFound, "Expected UserAgent extension in LOGIN7"); + Assert.Equal(0x1, observedVersion); + // Note: Accessing UserAgentInfo via Reflection. // We cannot use InternalsVisibleTo here because making internals visible to FunctionalTests // causes the *.TestHarness.cs stubs to clash with the real internal types in SqlClient. From d494b6635eb52ff1374fb3d63bfe91c6f26c3306 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Wed, 8 Oct 2025 15:56:13 -0700 Subject: [PATCH 11/19] Add useragent payload in parser --- .../Microsoft/Data/SqlClient/TdsParser.SSPI.cs | 10 +++++++++- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 18 +++++++++++++++++- .../SimulatedServerTests/ConnectionTests.cs | 15 ++++++++++++--- .../tools/TDS/TDS.Servers/GenericTdsServer.cs | 10 ---------- 4 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.SSPI.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.SSPI.cs index 6226f958a5..2881f70ad4 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.SSPI.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.SSPI.cs @@ -2,6 +2,7 @@ using System.Buffers; using System.Diagnostics; using System.Text; +using Microsoft.Data.SqlClient.UserAgent; using Microsoft.Data.SqlClient.Utilities; #nullable enable @@ -192,7 +193,14 @@ internal void TdsLogin( int feOffset = length; // calculate and reserve the required bytes for the featureEx - length = ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData, useFeatureExt, length); + length = ApplyFeatureExData( + requestedFeatures, + recoverySessionData, + fedAuthFeatureExtensionData, + UserAgentInfo.UserAgentCachedJsonPayload.ToArray(), + useFeatureExt, + length + ); WriteLoginData(rec, requestedFeatures, diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index 35df415a7c..b1e933f6e2 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -29,6 +29,8 @@ using Microsoft.Data.SqlClient.DataClassification; using Microsoft.Data.SqlClient.LocalDb; using Microsoft.Data.SqlClient.Server; +using Microsoft.Data.SqlClient.UserAgent; + #if NETFRAMEWORK using Microsoft.Data.SqlTypes; #endif @@ -9233,7 +9235,15 @@ private void WriteLoginData(SqlLogin rec, } } - ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData, useFeatureExt, length, true); + ApplyFeatureExData( + requestedFeatures, + recoverySessionData, + fedAuthFeatureExtensionData, + UserAgentInfo.UserAgentCachedJsonPayload.ToArray(), + useFeatureExt, + length, + true + ); } catch (Exception e) { @@ -9252,6 +9262,7 @@ private void WriteLoginData(SqlLogin rec, private int ApplyFeatureExData(TdsEnums.FeatureExtension requestedFeatures, SessionData recoverySessionData, FederatedAuthenticationFeatureExtensionData fedAuthFeatureExtensionData, + byte[] userAgentJsonPayload, bool useFeatureExt, int length, bool write = false) @@ -9306,6 +9317,11 @@ private int ApplyFeatureExData(TdsEnums.FeatureExtension requestedFeatures, length += WriteVectorSupportFeatureRequest(write); } + if ((requestedFeatures & TdsEnums.FeatureExtension.UserAgent) != 0) + { + length += WriteUserAgentFeatureRequest(userAgentJsonPayload, write); + } + length++; // for terminator if (write) { diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs index 34f3891263..0a1e83bb2e 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs @@ -15,6 +15,7 @@ using Microsoft.SqlServer.TDS; using Microsoft.SqlServer.TDS.FeatureExtAck; using Microsoft.SqlServer.TDS.Login7; +using Microsoft.SqlServer.TDS.PreLogin; using Microsoft.SqlServer.TDS.Servers; using Xunit; @@ -835,7 +836,8 @@ public void TestConnWithVectorFeatExtVersionNegotiation(bool expectedConnectionR [InlineData(true)] // Server is forced to send an Ack public void TestConnWithUserAgentFeatureExtension(bool forceAck) { - using var server = TestTdsServer.StartTestServer(); + using var server = new TdsServer(); + server.Start(); // Configure the server to support UserAgent version 0x01 server.ServerSupportedUserAgentFeatureExtVersion = 0x01; @@ -863,7 +865,7 @@ public void TestConnWithUserAgentFeatureExtension(bool forceAck) var data = token.Data; if (data == null || data.Length < 2) { - return; + return; } observedVersion = data[0]; @@ -871,8 +873,15 @@ public void TestConnWithUserAgentFeatureExtension(bool forceAck) loginFound = true; }; + // Connect to the test TDS server. + var connStr = new SqlConnectionStringBuilder + { + DataSource = $"localhost,{server.EndPoint.Port}", + Encrypt = SqlConnectionEncryptOption.Optional, + }.ConnectionString; + // TODO: Confirm the server sent an Ack by reading log message from SqlInternalConnectionTds - using var connection = new SqlConnection(server.ConnectionString); + using var connection = new SqlConnection(connStr); connection.Open(); // Verify the connection itself succeeded diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs index 22048be305..c3581753d2 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs @@ -132,16 +132,6 @@ public GenericTdsServer(T arguments, QueryEngine queryEngine) /// public int PreLoginCount => _preLoginCount; - /// - /// Property for setting server version for vector feature extension. - /// - public bool EnableVectorFeatureExt { get; set; } = false; - - /// - /// Property for setting server version for vector feature extension. - /// - public byte ServerSupportedVectorFeatureExtVersion { get; set; } = DefaultSupportedVectorFeatureExtVersion; - public OnAuthenticationCompletedDelegate OnAuthenticationResponseCompleted { private get; set; } public OnLogin7ValidatedDelegate OnLogin7Validated { private get; set; } From 4b59a62537e350e5270d84f91015cb88a5eb34e8 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Fri, 10 Oct 2025 14:19:19 -0700 Subject: [PATCH 12/19] Update Test --- .../Data/SqlClient/TdsParser.SSPI.cs | 5 ++++ .../src/Microsoft/Data/SqlClient/TdsParser.cs | 10 +++---- .../SimulatedServerTests/ConnectionTests.cs | 19 +++++-------- .../tools/TDS/TDS.Servers/GenericTdsServer.cs | 27 +++++++++++++++++++ 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.SSPI.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.SSPI.cs index 2881f70ad4..62cacada64 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.SSPI.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.SSPI.cs @@ -193,6 +193,11 @@ internal void TdsLogin( int feOffset = length; // calculate and reserve the required bytes for the featureEx + + // NOTE: This approach of pre-calculating the packet length is inefficient. + // We're making 2 passes over the data to be written. + // Instead, we should be writing everything to the buffer once, + // leaving a hole where the header length goes. length = ApplyFeatureExData( requestedFeatures, recoverySessionData, diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index b1e933f6e2..c6371f2e64 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -9271,6 +9271,11 @@ private int ApplyFeatureExData(TdsEnums.FeatureExtension requestedFeatures, { checked { + // NOTE: As part of TDS spec UserAgent feature extension should be the first feature extension in the list. + if ((requestedFeatures & TdsEnums.FeatureExtension.UserAgent) != 0) + { + length += WriteUserAgentFeatureRequest(userAgentJsonPayload, write); + } if ((requestedFeatures & TdsEnums.FeatureExtension.SessionRecovery) != 0) { length += WriteSessionRecoveryFeatureRequest(recoverySessionData, write); @@ -9317,11 +9322,6 @@ private int ApplyFeatureExData(TdsEnums.FeatureExtension requestedFeatures, length += WriteVectorSupportFeatureRequest(write); } - if ((requestedFeatures & TdsEnums.FeatureExtension.UserAgent) != 0) - { - length += WriteUserAgentFeatureRequest(userAgentJsonPayload, write); - } - length++; // for terminator if (write) { diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs index 0a1e83bb2e..3a9f28f582 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs @@ -854,20 +854,15 @@ public void TestConnWithUserAgentFeatureExtension(bool forceAck) // Inspect what the client sends in the LOGIN7 packet server.OnLogin7Validated = loginToken => { - var token = loginToken.FeatureExt - .OfType() - .FirstOrDefault(t => t.FeatureID == TDSFeatureID.UserAgentSupport); - if (token == null) - { - return; - } + var tdsFeatureExt = loginToken.FeatureExt + .OfType().ToArray(); + var token = tdsFeatureExt?.FirstOrDefault(t => t.FeatureID == TDSFeatureID.UserAgentSupport); + Assert.Equal(TDSFeatureID.UserAgentSupport, tdsFeatureExt?[0].FeatureID); + Assert.NotNull(token); var data = token.Data; - if (data == null || data.Length < 2) - { - return; - } - + Assert.True(data.Length >= 2); + observedVersion = data[0]; observedJsonBytes = data.AsSpan(1).ToArray(); loginFound = true; diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs index c3581753d2..b1311de17c 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs @@ -631,6 +631,7 @@ protected virtual TDSMessageCollection OnAuthenticationCompleted(ITDSServerSessi // Serialize the login token into the response packet responseMessage.Add(loginResponseToken); + // TODO: Split the conditional blocks below into separate virtual methods // Check if session recovery is enabled if (session.IsSessionRecoveryEnabled) { @@ -730,6 +731,32 @@ protected virtual TDSMessageCollection OnAuthenticationCompleted(ITDSServerSessi } } + //// If tests request it, force an ACK for UserAgentSupport with no negotiation + //if (session.EmitUserAgentFeatureExtAck) + //{ + // // Create ack data (1 byte: Version number) + // byte[] data = new byte[1]; + // data[0] = ServerSupportedVectorFeatureExtVersion > _clientSupportedVectorFeatureExtVersion ? _clientSupportedVectorFeatureExtVersion : ServerSupportedVectorFeatureExtVersion; + + // // Create vector support as a generic feature extension option + // TDSFeatureExtAckGenericOption vectorSupportOption = new TDSFeatureExtAckGenericOption(TDSFeatureID.VectorSupport, (uint)data.Length, data); + + // // Look for feature extension token + // TDSFeatureExtAckToken featureExtAckToken = (TDSFeatureExtAckToken)responseMessage.Where(t => t is TDSFeatureExtAckToken).FirstOrDefault(); + + // if (featureExtAckToken == null) + // { + // // Create feature extension ack token + // featureExtAckToken = new TDSFeatureExtAckToken(vectorSupportOption); + // responseMessage.Add(featureExtAckToken); + // } + // else + // { + // // Update the existing token + // featureExtAckToken.Options.Add(vectorSupportOption); + // } + //} + // Create DONE token TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final); From bd038a2681e313b07e8bdf3bcc1c0e2e5ebd8b11 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Fri, 10 Oct 2025 14:32:45 -0700 Subject: [PATCH 13/19] Remove stray comment --- .../src/Microsoft/Data/SqlClient/TdsEnums.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs index 5101c51649..7d6db2e560 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -241,7 +241,6 @@ public enum EnvChangeType : byte public const byte FEATUREEXT_SQLDNSCACHING = 0x0B; public const byte FEATUREEXT_JSONSUPPORT = 0x0D; public const byte FEATUREEXT_VECTORSUPPORT = 0x0E; - // TODO: re-verify if this byte competes with another feature public const byte FEATUREEXT_USERAGENT = 0x10; [Flags] From 92452b8a59cbffda60b109b17b3cda3bf092add8 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Fri, 10 Oct 2025 14:39:01 -0700 Subject: [PATCH 14/19] Cleanup --- .../tools/TDS/TDS.Servers/GenericTdsServer.cs | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs index b1311de17c..7e81e5a79e 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs @@ -731,32 +731,6 @@ protected virtual TDSMessageCollection OnAuthenticationCompleted(ITDSServerSessi } } - //// If tests request it, force an ACK for UserAgentSupport with no negotiation - //if (session.EmitUserAgentFeatureExtAck) - //{ - // // Create ack data (1 byte: Version number) - // byte[] data = new byte[1]; - // data[0] = ServerSupportedVectorFeatureExtVersion > _clientSupportedVectorFeatureExtVersion ? _clientSupportedVectorFeatureExtVersion : ServerSupportedVectorFeatureExtVersion; - - // // Create vector support as a generic feature extension option - // TDSFeatureExtAckGenericOption vectorSupportOption = new TDSFeatureExtAckGenericOption(TDSFeatureID.VectorSupport, (uint)data.Length, data); - - // // Look for feature extension token - // TDSFeatureExtAckToken featureExtAckToken = (TDSFeatureExtAckToken)responseMessage.Where(t => t is TDSFeatureExtAckToken).FirstOrDefault(); - - // if (featureExtAckToken == null) - // { - // // Create feature extension ack token - // featureExtAckToken = new TDSFeatureExtAckToken(vectorSupportOption); - // responseMessage.Add(featureExtAckToken); - // } - // else - // { - // // Update the existing token - // featureExtAckToken.Options.Add(vectorSupportOption); - // } - //} - // Create DONE token TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final); From 74027ae15b51ab20c964a0d6dae77b313a33dc85 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Fri, 10 Oct 2025 15:52:20 -0700 Subject: [PATCH 15/19] Make Uagent flag session based --- .../SimulatedServerTests/ConnectionTests.cs | 2 +- .../TDS/TDS.EndPoint/ITDSServerSession.cs | 5 + .../tools/TDS/TDS.Servers/GenericTdsServer.cs | 129 +++++++++++++----- .../TDS.Servers/GenericTdsServerSession.cs | 5 + 4 files changed, 103 insertions(+), 38 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs index 3a9f28f582..45b52cc0ca 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs @@ -843,7 +843,7 @@ public void TestConnWithUserAgentFeatureExtension(bool forceAck) server.ServerSupportedUserAgentFeatureExtVersion = 0x01; // Opt in to forced ACK for UserAgentSupport (no negotiation) - server.EmitUserAgentFeatureExtAck = forceAck; + server.EnableUserAgentFeatureExt = forceAck; bool loginFound = false; diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs index 9b5b7804b4..bb7b1c9771 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs @@ -93,5 +93,10 @@ public interface ITDSServerSession /// Indicates whether the client supports Vector column type /// bool IsVectorSupportEnabled { get; set; } + + /// + /// Indicates whether the client supports UserAgent + /// + bool IsUserAgentSupportEnabled { get; set; } } } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs index 7e81e5a79e..255b55ae8f 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs @@ -55,6 +55,11 @@ public delegate void OnAuthenticationCompletedDelegate( /// public bool EnableVectorFeatureExt { get; set; } = false; + /// + /// Property for setting server flag for user agent feature extension. + /// + public bool EnableUserAgentFeatureExt { get; set; } = false; + /// /// Property for setting server version for vector feature extension. /// @@ -329,6 +334,15 @@ public virtual TDSMessageCollection OnLogin7Request(ITDSServerSession session, T } break; } + case TDSFeatureID.UserAgentSupport: + { + if (EnableUserAgentFeatureExt) + { + // Enable User Agent Support + session.IsUserAgentSupportEnabled = true; + } + break; + } default: { @@ -631,7 +645,45 @@ protected virtual TDSMessageCollection OnAuthenticationCompleted(ITDSServerSessi // Serialize the login token into the response packet responseMessage.Add(loginResponseToken); - // TODO: Split the conditional blocks below into separate virtual methods + CheckSessionRecovery(session, responseMessage); + CheckJsonSupported(session, responseMessage); + CheckVectorSupport(session, responseMessage); + CheckUserAgentSupport(session, responseMessage); + + if (!string.IsNullOrEmpty(Arguments.FailoverPartner)) + { + envChange = new TDSEnvChangeToken(TDSEnvChangeTokenType.RealTimeLogShipping, Arguments.FailoverPartner); + + // Log response + TDSUtilities.Log(Arguments.Log, "Response", envChange); + + responseMessage.Add(envChange); + } + + // Create DONE token + TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final); + + // Log response + TDSUtilities.Log(Arguments.Log, "Response", doneToken); + + // Serialize DONE token into the response packet + responseMessage.Add(doneToken); + + // Invoke delegate for response validation + OnAuthenticationResponseCompleted?.Invoke(responseMessage); + + // Wrap a single message in a collection + return new TDSMessageCollection(responseMessage); + } + + + /// + /// Check if session recovery is enabled + /// + /// Server session + /// Response message + protected void CheckSessionRecovery(ITDSServerSession session, TDSMessage responseMessage) + { // Check if session recovery is enabled if (session.IsSessionRecoveryEnabled) { @@ -641,10 +693,19 @@ protected virtual TDSMessageCollection OnAuthenticationCompleted(ITDSServerSessi // Log response TDSUtilities.Log(Arguments.Log, "Response", featureExtActToken); - // Serialize feature extnesion token into the response + // Serialize feature extension token into the response responseMessage.Add(featureExtActToken); } + } + + /// + /// Check if Json is supported + /// + /// Server session + /// Response message + protected void CheckJsonSupported(ITDSServerSession session, TDSMessage responseMessage) + { // Check if Json is supported if (session.IsJsonSupportEnabled) { @@ -670,7 +731,15 @@ protected virtual TDSMessageCollection OnAuthenticationCompleted(ITDSServerSessi featureExtAckToken.Options.Add(jsonSupportOption); } } + } + /// + /// Check if Vector is supported + /// + /// Server session + /// Response message + protected void CheckVectorSupport(ITDSServerSession session, TDSMessage responseMessage) + { // Check if Vector is supported if (session.IsVectorSupportEnabled) { @@ -696,55 +765,41 @@ protected virtual TDSMessageCollection OnAuthenticationCompleted(ITDSServerSessi featureExtAckToken.Options.Add(vectorSupportOption); } } + } - if (!string.IsNullOrEmpty(Arguments.FailoverPartner)) - { - envChange = new TDSEnvChangeToken(TDSEnvChangeTokenType.RealTimeLogShipping, Arguments.FailoverPartner); - - // Log response - TDSUtilities.Log(Arguments.Log, "Response", envChange); - - responseMessage.Add(envChange); - } - + /// + /// Check if UserAgent support is enabled + /// + /// Server session + /// Response message + protected void CheckUserAgentSupport(ITDSServerSession session, TDSMessage responseMessage) + { // If tests request it, force an ACK for UserAgentSupport with no negotiation - if (EmitUserAgentFeatureExtAck) + if (session.IsUserAgentSupportEnabled) { - byte ackVersion = ServerSupportedUserAgentFeatureExtVersion; + // Create ack data (1 byte: Version number) + byte[] data = new byte[1]; + data[0] = ServerSupportedUserAgentFeatureExtVersion; - var data = new byte[] { ackVersion }; - var uaAck = new TDSFeatureExtAckGenericOption( - TDSFeatureID.UserAgentSupport, - (uint)data.Length, - data); + // Create user agent support as a generic feature extension option + TDSFeatureExtAckGenericOption userAgentSupportOption = new TDSFeatureExtAckGenericOption(TDSFeatureID.UserAgentSupport, (uint)data.Length, data); + + // Look for feature extension token + TDSFeatureExtAckToken featureExtAckToken = (TDSFeatureExtAckToken)responseMessage.Where(t => t is TDSFeatureExtAckToken).FirstOrDefault(); - // Reuse an existing FeatureExtAck token if present, otherwise add a new one - var featureExtAckToken = responseMessage.OfType().FirstOrDefault(); if (featureExtAckToken == null) { - featureExtAckToken = new TDSFeatureExtAckToken(uaAck); + // Create feature extension ack token + featureExtAckToken = new TDSFeatureExtAckToken(userAgentSupportOption); responseMessage.Add(featureExtAckToken); } else { - featureExtAckToken.Options.Add(uaAck); + // Update the existing token + featureExtAckToken.Options.Add(userAgentSupportOption); } } - // Create DONE token - TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final); - - // Log response - TDSUtilities.Log(Arguments.Log, "Response", doneToken); - - // Serialize DONE token into the response packet - responseMessage.Add(doneToken); - - // Invoke delegate for response validation - OnAuthenticationResponseCompleted?.Invoke(responseMessage); - - // Wrap a single message in a collection - return new TDSMessageCollection(responseMessage); } /// diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServerSession.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServerSession.cs index 2730fa02df..0c0b5fb67e 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServerSession.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServerSession.cs @@ -124,6 +124,11 @@ public class GenericTdsServerSession : ITDSServerSession /// public bool IsVectorSupportEnabled { get; set; } + /// + /// Indicates whether this session supports user agent + /// + public bool IsUserAgentSupportEnabled { get; set; } + #region Session Options /// From 4fc50559ca786468fcd0e8fac94fd6321588c518 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Fri, 10 Oct 2025 16:13:11 -0700 Subject: [PATCH 16/19] Minor fix --- .../tests/tools/TDS/TDS.Servers/GenericTdsServer.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs index 255b55ae8f..633f6edf0c 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs @@ -75,11 +75,6 @@ public delegate void OnAuthenticationCompletedDelegate( /// private byte _clientSupportedVectorFeatureExtVersion = 0; - /// - /// Server will ACK UserAgentSupport in the login response when this property is set to true. - /// - public bool EmitUserAgentFeatureExtAck { get; set; } = false; - /// /// Default feature extension version supported on the server for user agent. /// From 45b713231779ef282a2d7f67722eee563a83af22 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Mon, 13 Oct 2025 13:39:38 -0700 Subject: [PATCH 17/19] Add LocalAppContextSwitch and CI fix --- .../Data/SqlClient/LocalAppContextSwitches.cs | 24 ++++- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 10 +- .../Common/LocalAppContextSwitchesHelper.cs | 29 +++++- .../SimulatedServerTests/ConnectionTests.cs | 92 +++++++++++++++++-- 4 files changed, 138 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs index 31b5e66b98..f2ca2db2cc 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs @@ -25,6 +25,7 @@ private enum Tristate : byte private const string UseConnectionPoolV2String = @"Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2"; private const string TruncateScaledDecimalString = @"Switch.Microsoft.Data.SqlClient.TruncateScaledDecimal"; private const string IgnoreServerProvidedFailoverPartnerString = @"Switch.Microsoft.Data.SqlClient.IgnoreServerProvidedFailoverPartner"; + private const string EnableUserAgentString = @"Switch.Microsoft.Data.SqlClient.EnableUserAgent"; #if NET private const string GlobalizationInvariantModeString = @"System.Globalization.Invariant"; private const string GlobalizationInvariantModeEnvironmentVariable = "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"; @@ -45,6 +46,7 @@ private enum Tristate : byte private static Tristate s_useConnectionPoolV2; private static Tristate s_truncateScaledDecimal; private static Tristate s_ignoreServerProvidedFailoverPartner; + private static Tristate s_enableUserAgent; #if NET private static Tristate s_globalizationInvariantMode; private static Tristate s_useManagedNetworking; @@ -328,7 +330,27 @@ public static bool IgnoreServerProvidedFailoverPartner return s_ignoreServerProvidedFailoverPartner == Tristate.True; } } - + /// + /// When set to true, the user agent feature is enabled and the driver will send the user agent string to the server. + /// + public static bool EnableUserAgent + { + get + { + if (s_enableUserAgent == Tristate.NotInitialized) + { + if (AppContext.TryGetSwitch(EnableUserAgentString, out bool returnedValue) && returnedValue) + { + s_enableUserAgent = Tristate.True; + } + else + { + s_enableUserAgent = Tristate.False; + } + } + return s_enableUserAgent == Tristate.True; + } + } #if NET /// /// .NET Core 2.0 and up supports Globalization Invariant mode, which reduces the size of the required libraries for diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index 519ee9605e..eaf51f3fb2 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -9238,12 +9238,12 @@ private void WriteLoginData(SqlLogin rec, } ApplyFeatureExData( - requestedFeatures, - recoverySessionData, + requestedFeatures, + recoverySessionData, fedAuthFeatureExtensionData, UserAgentInfo.UserAgentCachedJsonPayload.ToArray(), - useFeatureExt, - length, + useFeatureExt, + length, true ); } @@ -9274,7 +9274,7 @@ private int ApplyFeatureExData(TdsEnums.FeatureExtension requestedFeatures, checked { // NOTE: As part of TDS spec UserAgent feature extension should be the first feature extension in the list. - if ((requestedFeatures & TdsEnums.FeatureExtension.UserAgent) != 0) + if (LocalAppContextSwitches.EnableUserAgent && ((requestedFeatures & TdsEnums.FeatureExtension.UserAgent) != 0)) { length += WriteUserAgentFeatureRequest(userAgentJsonPayload, write); } diff --git a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs index 2dea4ce022..0f7b994e5e 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs @@ -32,6 +32,7 @@ public sealed class LocalAppContextSwitchesHelper : IDisposable private readonly PropertyInfo _useConnectionPoolV2Property; private readonly PropertyInfo _truncateScaledDecimalProperty; private readonly PropertyInfo _ignoreServerProvidedFailoverPartner; + private readonly PropertyInfo _enableUserAgent; #if NET private readonly PropertyInfo _globalizationInvariantModeProperty; private readonly PropertyInfo _useManagedNetworkingProperty; @@ -60,6 +61,8 @@ public sealed class LocalAppContextSwitchesHelper : IDisposable private readonly Tristate _truncateScaledDecimalOriginal; private readonly FieldInfo _ignoreServerProvidedFailoverPartnerField; private readonly Tristate _ignoreServerProvidedFailoverPartnerOriginal; + private readonly FieldInfo _enableUserAgentField; + private readonly Tristate _enableUserAgentOriginal; #if NET private readonly FieldInfo _globalizationInvariantModeField; private readonly Tristate _globalizationInvariantModeOriginal; @@ -162,7 +165,11 @@ void InitProperty(string name, out PropertyInfo property) "IgnoreServerProvidedFailoverPartner", out _ignoreServerProvidedFailoverPartner); - #if NET + InitProperty( + "EnableUserAgent", + out _enableUserAgent); + +#if NET InitProperty( "GlobalizationInvariantMode", out _globalizationInvariantModeProperty); @@ -240,6 +247,11 @@ void InitField(string name, out FieldInfo field, out Tristate value) "s_ignoreServerProvidedFailoverPartner", out _ignoreServerProvidedFailoverPartnerField, out _ignoreServerProvidedFailoverPartnerOriginal); + + InitField( + "s_enableUserAgent", + out _enableUserAgentField, + out _enableUserAgentOriginal); #if NET InitField( @@ -323,6 +335,10 @@ void RestoreField(FieldInfo field, Tristate value) _ignoreServerProvidedFailoverPartnerField, _ignoreServerProvidedFailoverPartnerOriginal); + RestoreField( + _enableUserAgentField, + _enableUserAgentOriginal); + #if NET RestoreField( _globalizationInvariantModeField, @@ -429,6 +445,11 @@ public bool IgnoreServerProvidedFailoverPartner get => (bool)_ignoreServerProvidedFailoverPartner.GetValue(null); } + public bool EnableUserAgent + { + get => (bool)_enableUserAgent.GetValue(null); + } + #if NET /// /// Access the LocalAppContextSwitches.GlobalizationInvariantMode property. @@ -553,6 +574,12 @@ public Tristate IgnoreServerProvidedFailoverPartnerField set => SetValue(_ignoreServerProvidedFailoverPartnerField, value); } + public Tristate EnableUserAgentField + { + get => GetValue(_enableUserAgentField); + set => SetValue(_enableUserAgentField, value); + } + #if NET /// /// Get or set the LocalAppContextSwitches.GlobalizationInvariantMode switch value. diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs index 45b52cc0ca..9dad33aa1d 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs @@ -12,6 +12,7 @@ using System.Security; using System.Threading; using System.Threading.Tasks; +using Microsoft.Data.SqlClient.Tests.Common; using Microsoft.SqlServer.TDS; using Microsoft.SqlServer.TDS.FeatureExtAck; using Microsoft.SqlServer.TDS.Login7; @@ -836,6 +837,10 @@ public void TestConnWithVectorFeatExtVersionNegotiation(bool expectedConnectionR [InlineData(true)] // Server is forced to send an Ack public void TestConnWithUserAgentFeatureExtension(bool forceAck) { + // Make sure needed switch is enabled + using LocalAppContextSwitchesHelper switchesHelper = new(); + switchesHelper.EnableUserAgentField = LocalAppContextSwitchesHelper.Tristate.True; + using var server = new TdsServer(); server.Start(); @@ -851,6 +856,10 @@ public void TestConnWithUserAgentFeatureExtension(bool forceAck) byte observedVersion = 0; byte[] observedJsonBytes = Array.Empty(); + bool firstFeatureIsUserAgent = false; + bool tokenWasNotNull = false; + bool dataLengthAtLeast2 = false; + // Inspect what the client sends in the LOGIN7 packet server.OnLogin7Validated = loginToken => { @@ -858,13 +867,23 @@ public void TestConnWithUserAgentFeatureExtension(bool forceAck) .OfType().ToArray(); var token = tdsFeatureExt?.FirstOrDefault(t => t.FeatureID == TDSFeatureID.UserAgentSupport); - Assert.Equal(TDSFeatureID.UserAgentSupport, tdsFeatureExt?[0].FeatureID); - Assert.NotNull(token); - var data = token.Data; - Assert.True(data.Length >= 2); - - observedVersion = data[0]; - observedJsonBytes = data.AsSpan(1).ToArray(); + // Capture conditions instead of asserting here + firstFeatureIsUserAgent = tdsFeatureExt?.Length > 0 && tdsFeatureExt[0].FeatureID == TDSFeatureID.UserAgentSupport; + tokenWasNotNull = token is not null; + + var data = token?.Data ?? Array.Empty(); + dataLengthAtLeast2 = data.Length >= 2; + + if (data.Length >= 1) + { + observedVersion = data[0]; + } + + if (data.Length >= 2) + { + observedJsonBytes = data.AsSpan(1).ToArray(); + } + loginFound = true; }; @@ -874,7 +893,7 @@ public void TestConnWithUserAgentFeatureExtension(bool forceAck) DataSource = $"localhost,{server.EndPoint.Port}", Encrypt = SqlConnectionEncryptOption.Optional, }.ConnectionString; - + // TODO: Confirm the server sent an Ack by reading log message from SqlInternalConnectionTds using var connection = new SqlConnection(connStr); connection.Open(); @@ -882,8 +901,11 @@ public void TestConnWithUserAgentFeatureExtension(bool forceAck) // Verify the connection itself succeeded Assert.Equal(ConnectionState.Open, connection.State); - // Verify client did offer UserAgent + // Verify client did offer UserAgent and captured conditions hold Assert.True(loginFound, "Expected UserAgent extension in LOGIN7"); + Assert.True(firstFeatureIsUserAgent); + Assert.True(tokenWasNotNull); + Assert.True(dataLengthAtLeast2); Assert.Equal(0x1, observedVersion); // Note: Accessing UserAgentInfo via Reflection. @@ -895,7 +917,7 @@ public void TestConnWithUserAgentFeatureExtension(bool forceAck) asm.GetTypes().FirstOrDefault(t => t.FullName?.EndsWith(".UserAgentInfo", StringComparison.Ordinal) == true); Assert.NotNull(userAgentInfoType); - + // Try to get the property var prop = userAgentInfoType.GetProperty("UserAgentCachedJsonPayload", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); @@ -904,5 +926,55 @@ public void TestConnWithUserAgentFeatureExtension(bool forceAck) ReadOnlyMemory cachedPayload = (ReadOnlyMemory)prop.GetValue(null)!; Assert.Equal(cachedPayload.ToArray(), observedJsonBytes.ToArray()); } + + /// + /// Test to verify no UserAgent relevant information is sent when EnableUserAgentField switch is disabled. + /// + [Fact] + public void TestConnWithoutUserAgentFeatureExtension() + { + // Disable the client-side UserAgent field entirely + using LocalAppContextSwitchesHelper switchesHelper = new(); + switchesHelper.EnableUserAgentField = LocalAppContextSwitchesHelper.Tristate.False; + + using var server = new TdsServer(); + server.Start(); + + // Do not advertise or force the UserAgent feature on the server + server.ServerSupportedUserAgentFeatureExtVersion = 0x00; // no support + server.EnableUserAgentFeatureExt = false; // no forced ACK + + bool loginValidated = false; + bool userAgentFeatureSeen = false; + + // Inspect the LOGIN7 packet captured by the test server + server.OnLogin7Validated = loginToken => + { + var featureExtTokens = loginToken.FeatureExt + .OfType() + .ToArray(); + + // Ensure there is no UserAgentSupport token at all + var uaToken = featureExtTokens.FirstOrDefault(t => t.FeatureID == TDSFeatureID.UserAgentSupport); + userAgentFeatureSeen = uaToken is not null; + + loginValidated = true; + }; + + // Connect to the test TDS server with a basic connection string + var connStr = new SqlConnectionStringBuilder + { + DataSource = $"localhost,{server.EndPoint.Port}", + Encrypt = SqlConnectionEncryptOption.Optional, + }.ConnectionString; + + using var connection = new SqlConnection(connStr); + connection.Open(); + + // Verify that the connection succeeded and no UserAgent data was sent + Assert.Equal(ConnectionState.Open, connection.State); + Assert.True(loginValidated, "Expected LOGIN7 to be validated by the test server"); + Assert.False(userAgentFeatureSeen, "Did not expect a UserAgentSupport feature token in LOGIN7"); + } } } From f427f5d7844f1423560bda35efe35c7a22380797 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Mon, 13 Oct 2025 14:28:10 -0700 Subject: [PATCH 18/19] Minor fix --- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index 8ab3a3f704..d59ab67b3c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -29,6 +29,10 @@ using Microsoft.Data.SqlClient.DataClassification; using Microsoft.Data.SqlClient.LocalDb; using Microsoft.Data.SqlClient.Server; +using Microsoft.Data.SqlClient.UserAgent; +using Microsoft.Data.SqlClient.Utilities; + + #if NETFRAMEWORK using Microsoft.Data.SqlTypes; #endif @@ -1360,7 +1364,14 @@ internal void TdsLogin( int feOffset = length; // calculate and reserve the required bytes for the featureEx - length = ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData, useFeatureExt, length); + length = ApplyFeatureExData( + requestedFeatures, + recoverySessionData, + fedAuthFeatureExtensionData, + UserAgentInfo.UserAgentCachedJsonPayload.ToArray(), + useFeatureExt, + length + ); WriteLoginData(rec, requestedFeatures, From eb5410a4f8d03b81e67b20ec668348f092382c17 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Tue, 21 Oct 2025 15:12:32 -0700 Subject: [PATCH 19/19] Remove stray debug --- .../src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs | 2 -- 1 file changed, 2 deletions(-) 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 1e5ffbeef4..037ab671bb 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 @@ -1420,9 +1420,7 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, requestedFeatures |= TdsEnums.FeatureExtension.JsonSupport; requestedFeatures |= TdsEnums.FeatureExtension.VectorSupport; - #if DEBUG requestedFeatures |= TdsEnums.FeatureExtension.UserAgent; - #endif _parser.TdsLogin(login, requestedFeatures, _recoverySessionData, _fedAuthFeatureExtensionData, encrypt); }