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 01f4de3352..90899f35fc 100644
--- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj
+++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj
@@ -792,6 +792,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 8d178e11b6..fbfcbe6e44 100644
--- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj
+++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj
@@ -903,6 +903,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..d02394ef6f
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs
@@ -0,0 +1,318 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Data.Common;
+
+#nullable enable
+
+#if WINDOWS
+using System.Management;
+#endif
+
+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 driver name.
+ ///
+ private const int DriverNameMaxChars = 16;
+
+ ///
+ /// Maximum number of characters allowed for the driver version.
+ ///
+ private const int VersionMaxChars = 16;
+
+ ///
+ /// Maximum number of characters allowed for the operating system type.
+ ///
+ private const int OsTypeMaxChars = 16;
+
+ ///
+ /// Maximum number of characters allowed for the operating system details.
+ ///
+ private const int OsDetailsMaxChars = 128;
+
+ ///
+ /// Maximum number of characters allowed for the system architecture.
+ ///
+ private const int ArchMaxChars = 16;
+
+ ///
+ /// Maximum number of characters allowed for the driver runtime.
+ ///
+ private const int RuntimeMaxChars = 128;
+
+ ///
+ /// Maximum number of bytes allowed for the user agent json payload.
+ /// Payloads larger than this may be rejected by the server.
+ ///
+ public const int JsonPayloadMaxBytes = 2047;
+
+ private const string DefaultJsonValue = "Unknown";
+ private const string DriverName = "MS-MDS";
+
+ private static readonly UserAgentInfoDto _dto;
+ public static readonly byte[] _cachedPayload;
+
+ private enum OsType
+ {
+ Windows,
+ Linux,
+ macOS,
+ FreeBSD,
+ Android,
+ Unknown
+ }
+
+ static UserAgentInfo()
+ {
+ _dto = BuildDto();
+ _cachedPayload = AdjustJsonPayloadSize(_dto);
+ }
+
+ static UserAgentInfoDto BuildDto()
+ {
+ // 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 driverName = TruncateOrDefault(DriverName, DriverNameMaxChars);
+ var version = TruncateOrDefault(ADP.GetAssemblyVersion().ToString(), VersionMaxChars);
+ var osType = TruncateOrDefault(DetectOsType().ToString(), OsTypeMaxChars);
+ var osDetails = TruncateOrDefault(DetectOsDetails(), OsDetailsMaxChars);
+ var architecture = TruncateOrDefault(DetectArchitecture(), ArchMaxChars);
+ var runtime = TruncateOrDefault(DetectRuntime(), RuntimeMaxChars);
+
+ // Instantiate DTO before serializing
+ return new UserAgentInfoDto
+ {
+ Driver = driverName,
+ Version = version,
+ OS = new UserAgentInfoDto.OsInfo
+ {
+ Type = osType,
+ Details = osDetails
+ },
+ Arch = architecture,
+ Runtime = runtime
+
+ };
+
+ }
+
+ ///
+ /// 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)
+ {
+ 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
+ return JsonSerializer.SerializeToUtf8Bytes(dto, options);
+ }
+
+ ///
+ /// 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;
+ }
+ }
+
+ ///
+ /// 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
+ // second we fallback to OSPlatform checks
+#if NETCOREAPP3_0_OR_GREATER || NET5_0_OR_GREATER
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD))
+ return OsType.FreeBSD;
+#else
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("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;
+ }
+
+ ///
+ /// 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 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;
+ }
+
+ ///
+ /// 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();
+ }
+}
+
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..1dfe40d427
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfoDto.cs
@@ -0,0 +1,47 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+#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.
+ 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";
+
+
+ // TODO: Does this need to be nullable?
+ [JsonPropertyName(DriverJsonKey)]
+ public string? Driver { get; set; }
+
+ [JsonPropertyName(VersionJsonKey)]
+ public string? Version { get; set; }
+
+ [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; }
+
+ [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..b6faf84c9a
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs
@@ -0,0 +1,154 @@
+using System.Text;
+using System.Text.Json;
+using Microsoft.Data.SqlClient.UserAgent;
+using Xunit;
+
+#nullable enable
+
+namespace Microsoft.Data.SqlClient.UnitTests
+{
+ ///
+ /// Unit tests for and its companion DTO.
+ /// Focus areas:
+ /// 1. Field truncation logic
+ /// 2. Truncation verification
+ /// 3. Payload size adjustment and field dropping
+ /// 4. DTO JSON contract (key names and values)
+ /// 5. Combined truncation, adjustment, and serialization
+ ///
+ public class UserAgentInfoTests
+ {
+ // 1. Cached payload is within the 2,047‑byte spec and never null
+ [Fact]
+ public void CachedPayload_IsNotNull_And_WithinSpecLimit()
+ {
+ byte[] payload = UserAgentInfo._cachedPayload;
+ Assert.NotNull(payload);
+ Assert.InRange(payload.Length, 1, UserAgentInfo.JsonPayloadMaxBytes);
+ }
+
+ // 2. 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);
+ }
+
+ // 3. AdjustJsonPayloadSize drops low‑priority fields when required
+ [Fact]
+ public void AdjustJsonPayloadSize_StripsLowPriorityFields_When_PayloadTooLarge()
+ {
+ // Build an inflated DTO so AdjustJsonPayloadSize
+ // must fall back to its “drop fields” logic.
+ string huge = new string('x', 20_000);
+ var dto = new UserAgentInfoDto
+ {
+ Driver = huge,
+ Version = huge,
+ OS = new UserAgentInfoDto.OsInfo
+ {
+ Type = huge,
+ Details = huge
+ },
+ Arch = huge,
+ Runtime = huge
+ };
+
+ // Capture the size before the helper mutates the DTO
+ byte[] original = JsonSerializer.SerializeToUtf8Bytes(dto);
+ Assert.True(original.Length > UserAgentInfo.JsonPayloadMaxBytes);
+
+ // Run the field‑dropping logic
+ byte[] payload = UserAgentInfo.AdjustJsonPayloadSize(dto);
+ Assert.NotEmpty(payload);
+ Assert.True(payload.Length < original.Length); // verify shrinkage
+
+ // Structural checks using JsonDocument
+ using JsonDocument doc = JsonDocument.Parse(payload);
+ JsonElement root = doc.RootElement;
+
+ // High‑priority fields must survive.
+ Assert.True(root.TryGetProperty(UserAgentInfoDto.DriverJsonKey, out _));
+ Assert.True(root.TryGetProperty(UserAgentInfoDto.VersionJsonKey, out _));
+
+ // Low‑priority fields must be gone.
+ Assert.False(root.TryGetProperty(UserAgentInfoDto.ArchJsonKey, out _));
+ Assert.False(root.TryGetProperty(UserAgentInfoDto.RuntimeJsonKey, out _));
+
+ // If the "os" object survived, only its "type" sub‑field may remain.
+ if (root.TryGetProperty(UserAgentInfoDto.OsJsonKey, out JsonElement os))
+ {
+ Assert.True(os.TryGetProperty(UserAgentInfoDto.OsInfo.TypeJsonKey, out _));
+ Assert.False(os.TryGetProperty(UserAgentInfoDto.OsInfo.DetailsJsonKey, out _));
+ }
+ }
+
+ // 4. DTO JSON contract - verify names and values
+ [Fact]
+ public void Dto_JsonPropertyNames_MatchConstants()
+ {
+ var dto = new UserAgentInfoDto
+ {
+ Driver = "d",
+ Version = "v",
+ OS = new UserAgentInfoDto.OsInfo { Type = "t", Details = "dd" },
+ Arch = "a",
+ Runtime = "r"
+ };
+
+ string json = JsonSerializer.Serialize(dto);
+ using JsonDocument doc = JsonDocument.Parse(json);
+ JsonElement root = doc.RootElement;
+
+ // Assert – root‑level fields
+ Assert.Equal("d", root.GetProperty(UserAgentInfoDto.DriverJsonKey).GetString());
+ Assert.Equal("v", root.GetProperty(UserAgentInfoDto.VersionJsonKey).GetString());
+ Assert.Equal("a", root.GetProperty(UserAgentInfoDto.ArchJsonKey).GetString());
+ Assert.Equal("r", root.GetProperty(UserAgentInfoDto.RuntimeJsonKey).GetString());
+
+ // Assert – nested os object
+ JsonElement os = root.GetProperty(UserAgentInfoDto.OsJsonKey);
+ Assert.Equal("t", os.GetProperty(UserAgentInfoDto.OsInfo.TypeJsonKey).GetString());
+ Assert.Equal("dd", os.GetProperty(UserAgentInfoDto.OsInfo.DetailsJsonKey).GetString());
+ }
+
+ // 5. 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());
+ }
+ }
+}