diff --git a/spec/user-agent-v1.jsonc b/spec/user-agent-v1.jsonc
deleted file mode 100644
index d2c374df9f..0000000000
--- a/spec/user-agent-v1.jsonc
+++ /dev/null
@@ -1,94 +0,0 @@
-{
- // This is the JSON Schema that defines the format of the TDS USERAGENT
- // Feature Extension payload (version 1) sent to the server during login.
- //
- // Feature Extension Name: USERAGENT
- // Feature Extension Version: 1
- // Schema Version: 1
- //
- // The design document for version 1 is here:
- //
- // https://microsoft.sharepoint-df.com/:w:/t/sqldevx/ERIWTt0zlCxLroNHyaPlKYwBI_LNSff6iy_wXZ8xX6nctQ?e=iP8q75
-
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "urn:microsoft:mssql:user-agent:v1",
- "title": "Driver User Agent V1",
- "description": "The user agent payload sent by the driver during login.",
-
- "type": "object",
- "properties":
- {
- "driver":
- {
- "enum": ["MS-JDBC", "MS-MDS", "MS-ODBC", "MS-OLEDB", "MS-PHP", "MS-PYTHON"],
- "description": "The type of driver."
- },
- "version":
- {
- "type": "string",
- "description": "The version of the driver, as a semantic version.",
- // See:
- //
- // https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
- //
- "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
- },
- "os":
- {
- "type": "object",
- "description": "Information about the operating system.",
- "properties":
- {
- "type":
- {
- "enum": ["Windows", "Linux", "macOS", "FreeBSD", "Android", "Unknown"],
- "description": "The type of operating system."
- },
- "details":
- {
- "type": "string",
- "description": "Extended details of the operating system."
- }
- },
- "additionalProperties": false,
- "required": ["type", "details"]
- },
- "arch":
- {
- "type": "string",
- "description": "The architecture of the driver process."
- },
- "runtime":
- {
- "type": "string",
- "description": "The runtime environment of the driver."
- }
- },
- "additionalProperties": false,
- "required": ["driver", "version", "os", "arch", "runtime"],
- "examples":
- [
- {
- "driver": "MS-MDS",
- "version": "6.0.2",
- "os":
- {
- "type": "Linux",
- "details": "Debian GNU Linux 12.2 Bookworm"
- },
- "arch": "amd64",
- "runtime": ".NET 9.0.3"
- },
- {
- "driver": "MS-JDBC",
- "version": "11.2.0",
- "os":
- {
- "type": "Windows",
- "details": "Windows 10 Pro 22H2"
- },
- "arch": "x64",
- "runtime": "Java 17.0.8+7-LTS"
- }
- ]
-}
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 32b9e76c98..ede9922e96 100644
--- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj
+++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj
@@ -825,6 +825,9 @@
Microsoft\Data\SqlClient\TransactionRequest.cs
+
+ Microsoft\Data\SqlClient\UserAgent.cs
+
Microsoft\Data\SqlClient\VirtualSecureModeEnclaveProvider.cs
@@ -846,12 +849,6 @@
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 4bfe45ee72..f93c8d4bb4 100644
--- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj
+++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj
@@ -968,6 +968,9 @@
Microsoft\Data\SqlClient\TransactionRequest.cs
+
+ Microsoft\Data\SqlClient\UserAgent.cs
+
Microsoft\Data\SqlClient\Utilities\BufferWriterExtensions.netfx.cs
@@ -1001,12 +1004,6 @@
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/LocalAppContextSwitches.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs
index f2ca2db2cc..3a84ba050c 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs
@@ -330,8 +330,11 @@ 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.
+ /// When set to true, the user agent feature is enabled and the driver
+ /// will send the user agent string to the server as a LOGIN7 feature
+ /// extension.
///
public static bool EnableUserAgent
{
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlError.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlError.cs
index fc3a1247f3..a0d00956e9 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlError.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlError.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
+using Microsoft.Data.Common.ConnectionString;
namespace Microsoft.Data.SqlClient
{
@@ -11,7 +12,7 @@ namespace Microsoft.Data.SqlClient
public sealed class SqlError
{
// bug fix - MDAC 48965 - missing source of exception
- private readonly string _source = TdsEnums.SQL_PROVIDER_NAME;
+ private readonly string _source = DbConnectionStringDefaults.ApplicationName;
private readonly int _number;
private readonly byte _state;
private readonly byte _errorClass;
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs
index e070bb7d2e..772d6f2809 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs
@@ -10,6 +10,7 @@
using System.Globalization;
using System.Runtime.Serialization;
using System.Text;
+using Microsoft.Data.Common.ConnectionString;
namespace Microsoft.Data.SqlClient
{
@@ -112,7 +113,7 @@ public override void GetObjectData(SerializationInfo si, StreamingContext contex
public byte State => Errors.Count > 0 ? Errors[0].State : default;
///
- override public string Source => TdsEnums.SQL_PROVIDER_NAME;
+ override public string Source => DbConnectionStringDefaults.ApplicationName;
#if NET
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 f6df0a82fe..8931f445be 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs
@@ -13,10 +13,6 @@ namespace Microsoft.Data.SqlClient
internal static class TdsEnums
{
// internal tdsparser constants
-
-
- public const string SQL_PROVIDER_NAME = DbConnectionStringDefaults.ApplicationName;
-
public static readonly decimal SQL_SMALL_MONEY_MIN = new(-214748.3648);
public static readonly decimal SQL_SMALL_MONEY_MAX = new(214748.3647);
@@ -987,9 +983,6 @@ internal enum FedAuthInfoId : byte
internal const byte MAX_SUPPORTED_VECTOR_VERSION = 0x01;
internal const int VECTOR_HEADER_SIZE = 8;
- // User Agent constants
- internal const byte SUPPORTED_USER_AGENT_VERSION = 0x01;
-
// TCE Related constants
internal const byte MAX_SUPPORTED_TCE_VERSION = 0x03; // max version
internal const byte MIN_TCE_VERSION_WITH_ENCLAVE_SUPPORT = 0x02; // min version with enclave support
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 115c62f6c5..604d15797e 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs
@@ -18,13 +18,13 @@
using System.Xml;
using Interop.Common.Sni;
using Microsoft.Data.Common;
+using Microsoft.Data.Common.ConnectionString;
using Microsoft.Data.ProviderBase;
using Microsoft.Data.Sql;
using Microsoft.Data.SqlClient.Connection;
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;
using Microsoft.SqlServer.Server;
@@ -1305,11 +1305,11 @@ internal void TdsLogin(
// length in bytes
int length = TdsEnums.SQL2005_LOG_REC_FIXED_LEN;
- string clientInterfaceName = TdsEnums.SQL_PROVIDER_NAME;
- Debug.Assert(TdsEnums.MAXLEN_CLIENTINTERFACE >= clientInterfaceName.Length, "cchCltIntName can specify at most 128 unicode characters. See Tds spec");
+ // Obtain the client interface name.
+ string clientInterfaceName = DbConnectionStringDefaults.ApplicationName;
+ Debug.Assert(clientInterfaceName.Length <= TdsEnums.MAXLEN_CLIENTINTERFACE);
// add up variable-len portions (multiply by 2 for byte len of char strings)
- //
checked
{
length += (rec.hostName.Length + rec.applicationName.Length +
@@ -1364,7 +1364,7 @@ internal void TdsLogin(
requestedFeatures,
recoverySessionData,
fedAuthFeatureExtensionData,
- UserAgentInfo.UserAgentCachedJsonPayload.ToArray(),
+ UserAgent.Ucs2Bytes,
useFeatureExt,
length
);
@@ -9172,29 +9172,30 @@ internal int WriteVectorSupportFeatureRequest(bool write)
}
///
- /// Writes the User Agent feature request to the physical state object.
- /// The request includes the feature ID, feature data length, version number and encoded JSON payload.
+ /// Writes the User Agent feature request to the physical state
+ /// object. The request includes the feature ID, feature data length,
+ /// and UCS-2 little-endian encoded payload.
///
- /// Byte array of UTF-8 encoded JSON payload for User Agent
+ ///
+ /// The feature request consists of:
+ /// - 1 byte for the feature ID.
+ /// - 4 bytes for the feature data length.
+ /// - N bytes for the UCS-2 payload
+ ///
+ ///
+ /// UCS-2 little-endian encoded UserAgent payload.
+ ///
///
- /// If true, writes the feature request to the physical state object.
- /// If false, just calculates the length.
+ /// If true, writes the feature request to the physical state object.
+ /// If false, just calculates the length.
///
/// The length of the feature request in bytes.
- ///
- /// The feature request consists of:
- /// - 1 byte for the feature ID.
- /// - 4 bytes for the feature data length.
- /// - 1 byte for the version number.
- /// - N bytes for the JSON payload
- ///
- internal int WriteUserAgentFeatureRequest(byte[] userAgentJsonPayload,
+ internal int WriteUserAgentFeatureRequest(ReadOnlyMemory userAgent,
bool write)
{
- // 1byte (Feature Version) + size of UTF-8 encoded JSON payload
- int dataLen = 1 + userAgentJsonPayload.Length;
- // 1byte (Feature ID) + 4bytes (Feature Data Length) + 1byte (Version) + N(JSON payload size)
- int totalLen = 1 + 4 + dataLen;
+ // 1 byte (Feature ID) + 4 bytes (Feature Data Length) + N bytes
+ // (UCS-2 payload size)
+ int totalLen = 1 + 4 + userAgent.Length;
if (write)
{
@@ -9202,13 +9203,10 @@ internal int WriteUserAgentFeatureRequest(byte[] userAgentJsonPayload,
_physicalStateObj.WriteByte(TdsEnums.FEATUREEXT_USERAGENT);
// Feature Data Length
- WriteInt(dataLen, _physicalStateObj);
-
- // Write Feature Version
- _physicalStateObj.WriteByte(TdsEnums.SUPPORTED_USER_AGENT_VERSION);
+ WriteInt(userAgent.Length, _physicalStateObj);
- // Write encoded JSON payload
- _physicalStateObj.WriteByteArray(userAgentJsonPayload, userAgentJsonPayload.Length, 0);
+ // Write encoded UCS-2 payload
+ _physicalStateObj.WriteByteSpan(userAgent.Span);
}
return totalLen;
@@ -9488,7 +9486,7 @@ private void WriteLoginData(SqlLogin rec,
requestedFeatures,
recoverySessionData,
fedAuthFeatureExtensionData,
- UserAgentInfo.UserAgentCachedJsonPayload.ToArray(),
+ UserAgent.Ucs2Bytes,
useFeatureExt,
length,
true
@@ -9511,7 +9509,7 @@ private void WriteLoginData(SqlLogin rec,
private int ApplyFeatureExData(TdsEnums.FeatureExtension requestedFeatures,
SessionData recoverySessionData,
FederatedAuthenticationFeatureExtensionData fedAuthFeatureExtensionData,
- byte[] userAgentJsonPayload,
+ ReadOnlyMemory userAgent,
bool useFeatureExt,
int length,
bool write = false)
@@ -9523,7 +9521,7 @@ private int ApplyFeatureExData(TdsEnums.FeatureExtension requestedFeatures,
// NOTE: As part of TDS spec UserAgent feature extension should be the first feature extension in the list.
if (LocalAppContextSwitches.EnableUserAgent && ((requestedFeatures & TdsEnums.FeatureExtension.UserAgent) != 0))
{
- length += WriteUserAgentFeatureRequest(userAgentJsonPayload, write);
+ length += WriteUserAgentFeatureRequest(userAgent, write);
}
if ((requestedFeatures & TdsEnums.FeatureExtension.SessionRecovery) != 0)
{
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent.cs
new file mode 100644
index 0000000000..55bc3b8208
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent.cs
@@ -0,0 +1,433 @@
+// 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;
+
+#nullable enable
+
+namespace Microsoft.Data.SqlClient;
+
+///
+/// This class uses runtime environment information to produce a value
+/// suitable for use in the TDS LOGIN7 USERAGENT Feature Extension
+/// payload.
+///
+/// See the spec here:
+///
+///
+/// SQL Drivers User Agent V1
+///
+///
+internal static class UserAgent
+{
+ #region Properties
+
+ ///
+ ///
+ /// The pipe-delimited payload as a string, never null, never empty, and
+ /// never larger than 256 characters.
+ ///
+ ///
+ /// The format is pipe ('|') delimited into 7 parts:
+ ///
+ /// 1|MS-MDS|{Driver Version}|{Arch}|{OS Type}|{OS Info}|{Runtime Info}
+ ///
+ ///
+ /// The {Driver Version} part is the version of the driver,
+ /// sourced from the MDS NuGet package version in SemVer 2.0 format.
+ /// Maximum length is 24 characters.
+ ///
+ ///
+ /// The {Arch} part will be the process architecture, either
+ /// the bare metal hardware architecture or the virtualized
+ /// architecture. See
+ ///
+ /// ProcessArchitecture
+ ///
+ /// for possible values. Maximum length is 10 characters.
+ ///
+ ///
+ /// The {OS Type} part will be one of the following strings:
+ ///
+ /// Windows
+ /// Linux
+ /// macOS
+ /// FreeBSD
+ /// Unknown
+ ///
+ ///
+ ///
+ /// The {OS Info} part will be sourced from the
+ ///
+ /// OSDescription
+ ///
+ /// value, or "Unknown" if that value is empty or all whitespace.
+ /// Maximum length is 44 characters.
+ ///
+ ///
+ /// The {Runtime Info} part will be sourced from the
+ ///
+ /// FrameworkDescription
+ ///
+ /// value, or "Unknown" if that value is empty or all whitespace.
+ /// Maximum length is 44 characters.
+ ///
+ ///
+ /// Any characters from the sourced values that are not one of the
+ /// following are replaced with underscore ('_'):
+ ///
+ ///
+ /// ASCII letters ([A-za-z])
+ ///
+ /// ASCII digits ([0-9])
+ /// Space (' ')
+ /// Period ('.')
+ /// Plus ('+')
+ /// Underscore ('_')
+ /// Hyphen ('-')
+ ///
+ ///
+ ///
+ /// All known exceptions are caught and handled by injecting the
+ /// fallback value of "Unknown". However, no effort is made to
+ /// catch all exceptions, for example process-fatal memory
+ /// allocation errors.
+ ///
+ ///
+ internal static string Value { get; }
+
+ ///
+ /// The Value as UCS-2 encoded bytes.
+ ///
+ internal static ReadOnlyMemory Ucs2Bytes { get; }
+
+ #endregion Properties
+
+ #region Helpers
+
+ ///
+ /// Static construction builds the Client Interface Name. All known
+ /// exceptions are consumed.
+ ///
+ static UserAgent()
+ {
+ // Determine the OS type.
+ //
+ // This is done outside of Build() to allow tests to inject
+ // specific values.
+ //
+ string osType = Unknown;
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ osType = Windows;
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ osType = Linux;
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ osType = macOS;
+ }
+ // The FreeBSD platform doesn't exist in .NET Framework at all.
+ #if NET
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD))
+ {
+ osType = FreeBSD;
+ }
+ #endif
+
+ // Build it!
+ Value = Build(
+ MaxLenOverall,
+ PayloadVersion,
+ DriverName,
+ System.ThisAssembly.NuGetPackageVersion,
+ RuntimeInformation.ProcessArchitecture,
+ osType,
+ RuntimeInformation.OSDescription,
+ RuntimeInformation.FrameworkDescription);
+
+ // Convert it to UCS-2 bytes.
+ //
+ // The default Unicode instance doesn't throw if encoding fails, so
+ // there is nothing to catch here.
+ Ucs2Bytes = Encoding.Unicode.GetBytes(Value);
+ }
+
+ ///
+ /// Build the payload string value and return it.
+ ///
+ /// The length of the returned value will never be longer than
+ /// .
+ ///
+ /// All known exceptions are consumed.
+ ///
+ ///
+ /// The maximum length of the returned value.
+ ///
+ ///
+ /// The value of the payload version part.
+ ///
+ ///
+ /// The value of the driver name part.
+ ///
+ ///
+ /// The value of the driver version part.
+ ///
+ ///
+ /// The value of the Architecture part.
+ ///
+ ///
+ /// The value of the OS Type part.
+ ///
+ ///
+ /// The value of the OS Info part.
+ ///
+ ///
+ /// The value of the Runtime Info part.
+ ///
+ ///
+ /// The payload string value, never null, never empty, and never longer
+ /// than .
+ ///
+ internal static string Build(
+ ushort maxLen,
+ string payloadVersion,
+ string driverName,
+ string driverVersion,
+ Architecture arch,
+ string osType,
+ string osInfo,
+ string runtimeInfo)
+ {
+ string result;
+
+ // Clean and truncate the payload version and driver name. We will need
+ // them for error handling.
+ payloadVersion = Truncate(Clean(payloadVersion), MaxLenPayloadVersion);
+ driverName = Truncate(Clean(driverName), MaxLenDriverName);
+
+ try
+ {
+ // Expect to build a string whose length is up to our max length.
+ //
+ // This isn't a max capacity, but a hint for initial buffer
+ // allocation. We will truncate to our max length after all of the
+ // pieces have been appended.
+ //
+ StringBuilder name = new StringBuilder(maxLen);
+
+ // Start with the (clean) payload version and driver name.
+ name.Append(payloadVersion);
+ name.Append('|');
+ name.Append(driverName);
+ name.Append('|');
+
+ // Add the Driver Version, truncating to its max length.
+ name.Append(Truncate(Clean(driverVersion), MaxLenDriverVersion));
+ name.Append('|');
+
+ // Add the Architecture, truncating to its max length.
+ name.Append(Truncate(Clean(arch.ToString()), MaxLenArch));
+ name.Append('|');
+
+ // Add the OS Type, truncating to its max length.
+ name.Append(Truncate(Clean(osType), MaxLenOsType));
+ name.Append('|');
+
+ // Add the OS Info, truncating to its max length.
+ name.Append(Truncate(Clean(osInfo), MaxLenOsInfo));
+ name.Append('|');
+
+ // Add the Runtime Info, truncating to its max length.
+ name.Append(Truncate(Clean(runtimeInfo), MaxLenRuntimeInfo));
+
+ // Remember the name we've built up.
+ result = name.ToString();
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ // StringBuilder failed in an unexpected way, so use our fallback
+ // value.
+ result =
+ $"{payloadVersion}|{driverName}|{Unknown}|{Unknown}|" +
+ $"{Unknown}|{Unknown}|{Unknown}";
+ }
+
+ // Truncate to our max length if necessary.
+ //
+ // This is a paranoia check to ensure we don't violate our API
+ // promise.
+ //
+ if (result.Length > maxLen)
+ {
+ // We know this won't throw ArgumentOutOfRangeException because
+ // we've already confirmed that Length is greater than maxLen.
+ result = result.Substring(0, maxLen);
+ }
+
+ return result;
+ }
+
+ ///
+ ///
+ /// Clean the given value of any disallowed characters, replacing them
+ /// with underscore ('_'), and return the cleaned value.
+ ///
+ /// Leading and trailing whitespace are removed.
+ ///
+ /// Each disallowed character is replaced with an underscore, preserving
+ /// the original length of the value. No effort is made to collapse
+ /// adjacent disallowed characters.
+ ///
+ ///
+ /// Permitted characters are:
+ ///
+ ///
+ /// ASCII letters ([A-za-z])
+ ///
+ /// ASCII digits ([0-9])
+ /// Space (' ')
+ /// Period ('.')
+ /// Plus ('+')
+ /// Underscore ('_')
+ /// Hyphen ('-')
+ ///
+ ///
+ ///
+ /// If the given value is null, empty, or all whitespace, or an error
+ /// occurs, the fallback value is returned.
+ ///
+ ///
+ /// The value to clean.
+ ///
+ /// The cleaned value, or the fallback value if any errors occur.
+ ///
+ internal static string Clean(string? value)
+ {
+ if (string.IsNullOrWhiteSpace(value)
+ // .NET Framework doesn't consider IsNullOrWhiteSpace()
+ // sufficient for nullable checks, so add an explicit check for
+ // null.
+ #if NETFRAMEWORK
+ || value == null
+ #endif
+ )
+ {
+ return Unknown;
+ }
+
+ // Remove any leading and trailing whitespace.
+ value = value.Trim();
+
+ try
+ {
+ // Build the cleaned value by hand, avoiding the overhead and
+ // failure scenarios of regexes or other more complex solutions.
+ //
+ // We expect the value to be short, and this code is called only a
+ // few times per process. Robustness and simplicity are more
+ // important than performance here.
+ //
+ StringBuilder cleaned = new StringBuilder(value.Length);
+ foreach (char c in value)
+ {
+ // Is it a permitted character?
+ if (
+ #if NET
+ char.IsAsciiLetter(c)
+ || char.IsAsciiDigit(c)
+ #else
+ (c >= 'A' && c <= 'Z')
+ || (c >= 'a' && c <= 'z')
+ || (c >= '0' && c <= '9')
+ #endif
+ || c == ' '
+ || c == '.'
+ || c == '+'
+ || c == '_'
+ || c == '-')
+ {
+ // Yes, so append it as-is.
+ cleaned.Append(c);
+ }
+ else
+ {
+ // No, so replace it with an underscore.
+ cleaned.Append('_');
+ }
+ }
+
+ return cleaned.ToString();
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ // StringBuilder failed in an unexpected way, so use our fallback
+ // value.
+ return Unknown;
+ }
+ }
+
+ ///
+ /// Truncate the given value to the given max length, and return the
+ /// result.
+ ///
+ /// The value to truncate.
+ /// The maximum length to truncate to.
+ ///
+ /// The truncated value, or the original if no
+ /// truncation occurred.
+ ///
+ internal static string Truncate(string value, ushort maxLength)
+ {
+ if (value.Length <= maxLength)
+ {
+ return value;
+ }
+
+ // We know this won't throw ArgumentOutOfRangeException because we've
+ // already confirmed that Length is greater than maxLength.
+ return value.Substring(0, maxLength);
+ }
+
+ #endregion Helpers
+
+ #region Private Fields
+
+ // Our payload format version.
+ private const string PayloadVersion = "1";
+
+ // Our well-known .NET driver name.
+ private const string DriverName = "MS-MDS";
+
+ // The overall maximum length of Value.
+ private const ushort MaxLenOverall = 256;
+
+ // Maximum part lengths as promised in our API.
+ private const ushort MaxLenPayloadVersion = 2;
+ private const ushort MaxLenDriverName = 12;
+ private const ushort MaxLenDriverVersion = 24;
+ private const ushort MaxLenArch = 10;
+ private const ushort MaxLenOsType = 10;
+ private const ushort MaxLenOsInfo = 44;
+ private const ushort MaxLenRuntimeInfo = 44;
+
+ // The OS Type values we promise in our API.
+ private const string Windows = "Windows";
+ private const string Linux = "Linux";
+ private const string macOS = "macOS";
+ // The FreeBSD platform doesn't exist in .NET Framework at all.
+ #if NET
+ private const string FreeBSD = "FreeBSD";
+ #endif
+
+ // A fallback value for parts of the client interface name that are
+ // unknown, invalid, or when errors occur.
+ private const string Unknown = "Unknown";
+
+ #endregion Private Fields
+}
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
deleted file mode 100644
index d927155dbb..0000000000
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs
+++ /dev/null
@@ -1,361 +0,0 @@
-// 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 (maxChars <= 0)
- {
- 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
deleted file mode 100644
index 2c61d1c4bb..0000000000
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfoDto.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-// 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/Microsoft/Data/SqlClient/UdtSerialization/NativeSerializationTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/NativeSerializationTest.cs
index 3369a66985..bcc1acbc2e 100644
--- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/NativeSerializationTest.cs
+++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/NativeSerializationTest.cs
@@ -384,7 +384,9 @@ public static TheoryData