diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlClientApp.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlClientApp.xml
new file mode 100644
index 0000000000..a5cad14cf9
--- /dev/null
+++ b/doc/snippets/Microsoft.Data.SqlClient/SqlClientApp.xml
@@ -0,0 +1,141 @@
+
+
+
+
+ Specifies the known application identifiers that Microsoft.Data.SqlClient reports for user agent telemetry.
+
+
+
+ Production applications that meet the bar are welcome to reserve an identifier here.
+
+
+ Identifier reservations are as follows:
+
+
+ -
+ 0x0001-0x7FFF: Microsoft-defined large-scale applications.
+
+ -
+ 0x8000-0xBFFF: Reserved for small-scale use.
+
+ -
+ 0xC000-0xFFFF: Public and developer use.
+
+
+
+ An unregistered identifier may still be reported by casting a value to this type. Identifiers are limited to
+ the 16-bit space the protocol allows, so a value outside 0x0000 to 0xFFFF is rejected when it is assigned to
+ .
+
+
+
+
+
+ No application identity is reported. This is the default.
+
+
+ 0
+
+
+
+
+ The Microsoft Entity Framework Core SQL Server provider.
+
+
+ 1
+
+
+
+
+ Microsoft Semantic Kernel.
+
+
+ 2
+
+
+
+
+ Microsoft SQL Server Management Studio.
+
+
+ 3
+
+
+
+
+ Microsoft SQL Server Management Objects.
+
+
+ 4
+
+
+
+
+ Microsoft SQL Server Data-Tier Application Framework.
+
+
+ 5
+
+
+
+
+ Microsoft SQL Tools Service.
+
+
+ 6
+
+
+
+
+ Microsoft ASP.NET Core distributed SQL Server cache.
+
+
+ 7
+
+
+
+
+ Microsoft Entity Framework 6 SQL Server provider.
+
+
+ 8
+
+
+
+
+ Microsoft Azure Functions SQL extension.
+
+
+ 9
+
+
+
+
+ Microsoft Orleans ADO.NET providers.
+
+
+ 10
+
+
+
+
+ Microsoft Durable Task SQL Server provider.
+
+
+ 11
+
+
+
+
+ The sqlpackage command-line tool.
+
+
+ 12
+
+
+ sqlpackage is built on the Data-Tier Application Framework, but reports its own identifier so that
+ command-line use can be told apart from other callers of that framework.
+
+
+
+
diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml
index 5f88a1498e..7f91f099d4 100644
--- a/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml
+++ b/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml
@@ -2294,6 +2294,57 @@ The following sample tries to open a connection to an invalid database to simula
+
+
+ Gets or sets the middleware application identity reported to the server for this connection.
+
+
+ A value. The default is
+ .
+
+
+
+ Set the identity before opening the connection:
+
+
+ using Microsoft.Data.SqlClient;
+
+ using var connection = new SqlConnection(connectionString);
+ connection.SqlClientAppId = SqlClientApp.EntityFramework;
+ connection.Open();
+
+
+
+ The value is outside the range 0 to 65535.
+
+
+ The connection is opening or open. The identity is reported during login, so it must be set beforehand.
+
+
+
+ This API is intended for registered applications that reserve an identifier in
+ . An unregistered identifier may be reported by casting
+ a value to that type, provided it is within the 16-bit range the protocol allows.
+
+
+ This value is telemetry. It is supplied entirely by the client, which may report any identifier in range,
+ so it is not an authenticated identity and must not be used for authorization or any other security
+ decision.
+
+
+ The identity is sent once, during login, so it must be set before the connection is opened.
+
+
+ When pooling is enabled the value is reported only while establishing a new physical connection, and it is
+ not part of the pool key. A connection served from the pool therefore reports the identity of whichever
+ connection caused that physical connection to be created, and physical connections opened in the background
+ to satisfy Min Pool Size report
+ . Applications that mix identities over one
+ connection string should treat this telemetry as indicative rather than exact, or disable pooling where an
+ exact attribution is required.
+
+
+
Gets a string that identifies the database client.
diff --git a/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs
index 266d0b9a1d..b88678a750 100644
--- a/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs
+++ b/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs
@@ -592,6 +592,37 @@ public void LogError(string type, string method, string message) { }
public void LogInfo(string type, string method, string message) { }
}
+///
+public enum SqlClientApp
+{
+ ///
+ Unknown = 0x0000,
+ ///
+ EntityFramework = 0x0001,
+ ///
+ SemanticKernel = 0x0002,
+ ///
+ ManagementStudio = 0x0003,
+ ///
+ SqlManagementObjects = 0x0004,
+ ///
+ DataTierApplicationFramework = 0x0005,
+ ///
+ SqlToolsService = 0x0006,
+ ///
+ AspNetCoreDistributedSqlServerCache = 0x0007,
+ ///
+ EntityFramework6 = 0x0008,
+ ///
+ AzureFunctionsSqlExtension = 0x0009,
+ ///
+ OrleansAdoNet = 0x000A,
+ ///
+ DurableTaskSqlServer = 0x000B,
+ ///
+ SqlPackage = 0x000C
+}
+
///
public static class SqlClientMetaDataCollectionNames
{
@@ -1010,6 +1041,8 @@ public SqlConnection() { }
public SqlConnection(string connectionString) { }
///
public SqlConnection(string connectionString, Microsoft.Data.SqlClient.SqlCredential credential) { }
+ ///
+ public Microsoft.Data.SqlClient.SqlClientApp SqlClientAppId { get { throw null; } set { } }
///
[System.ComponentModel.BrowsableAttribute(false)]
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs
index ec210e407a..5351e48c8c 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs
@@ -306,6 +306,12 @@ internal class SqlConnectionInternal : DbConnectionInternal, IDisposable
private bool _sessionRecoveryRequested;
+ ///
+ /// The middleware application identity of the that caused
+ /// this physical connection to be created. Reported once, at login.
+ ///
+ private readonly SqlClientApp _sqlClientAppId;
+
private int _threadIdOwningParserLock = -1;
// @TODO: Rename to indicate this has to do with routing
@@ -344,12 +350,14 @@ internal SqlConnectionInternal(
IDbConnectionPool pool = null,
Func> accessTokenCallback = null,
SspiContextProvider sspiContextProvider = null,
- ISqlClientMetrics metrics = null)
+ ISqlClientMetrics metrics = null,
+ SqlClientApp sqlClientAppId = SqlClientApp.Unknown)
: base(metrics)
{
Debug.Assert(connectionOptions is not null, "null connectionOptions");
ConnectionOptions = connectionOptions;
+ _sqlClientAppId = sqlClientAppId;
#if DEBUG
if (reconnectSessionData != null)
@@ -3063,6 +3071,7 @@ private void Login(
login.password = ConnectionOptions.Password;
login.applicationName = ConnectionOptions.ApplicationName;
login.language = _currentLanguage;
+ login.appId = _sqlClientAppId;
if (!login.userInstance)
{
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientApp.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientApp.cs
new file mode 100644
index 0000000000..55cdea2b9d
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientApp.cs
@@ -0,0 +1,38 @@
+// 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.
+
+#nullable enable
+
+namespace Microsoft.Data.SqlClient;
+
+///
+public enum SqlClientApp
+{
+ ///
+ Unknown = 0x0000,
+ ///
+ EntityFramework = 0x0001,
+ ///
+ SemanticKernel = 0x0002,
+ ///
+ ManagementStudio = 0x0003,
+ ///
+ SqlManagementObjects = 0x0004,
+ ///
+ DataTierApplicationFramework = 0x0005,
+ ///
+ SqlToolsService = 0x0006,
+ ///
+ AspNetCoreDistributedSqlServerCache = 0x0007,
+ ///
+ EntityFramework6 = 0x0008,
+ ///
+ AzureFunctionsSqlExtension = 0x0009,
+ ///
+ OrleansAdoNet = 0x000A,
+ ///
+ DurableTaskSqlServer = 0x000B,
+ ///
+ SqlPackage = 0x000C
+}
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientDriverProperties.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientDriverProperties.cs
new file mode 100644
index 0000000000..6975475c31
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientDriverProperties.cs
@@ -0,0 +1,75 @@
+// 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;
+
+#nullable enable
+
+namespace Microsoft.Data.SqlClient;
+
+///
+/// Driver-owned feature flags reported in the Driver Properties part of the
+/// USERAGENT login feature extension payload.
+///
+///
+/// This part is driver-owned, so its meaning is defined entirely by
+/// Microsoft.Data.SqlClient and carries no cross-driver contract. Other
+/// drivers use the same part for their own purposes.
+///
+[Flags]
+internal enum SqlClientDriverProperties : ushort
+{
+ /// No tracked features are enabled.
+ None = 0x0000,
+
+ ///
+ /// The connection pool V2 implementation
+ /// (Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2) is
+ /// enabled.
+ ///
+ ConnectionPoolV2 = 0x0001
+}
+
+///
+/// Resolves the flags that describe
+/// how this process is configured.
+///
+internal static class SqlClientDriverPropertiesResolver
+{
+ ///
+ /// The flags describing the current process.
+ ///
+ ///
+ /// The flags are sourced from process-wide switches, so this is stable
+ /// for the life of the process.
+ ///
+ internal static SqlClientDriverProperties Current =>
+ Resolve(LocalAppContextSwitches.UseConnectionPoolV2);
+
+ ///
+ /// Maps the process configuration to the flags that describe it.
+ ///
+ ///
+ /// Whether the connection pool V2 implementation is enabled.
+ ///
+ ///
+ /// The flags describing the supplied configuration.
+ ///
+ ///
+ /// The mapping is kept separate from because the
+ /// switches it reads are cached for the life of the process, which makes
+ /// them impractical to vary in a test.
+ ///
+ internal static SqlClientDriverProperties Resolve(bool useConnectionPoolV2)
+ {
+ SqlClientDriverProperties properties = SqlClientDriverProperties.None;
+
+ if (useConnectionPoolV2)
+ {
+ properties |= SqlClientDriverProperties.ConnectionPoolV2;
+ }
+
+ return properties;
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs
index b8b693132e..a8a41bdcd6 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs
@@ -75,6 +75,7 @@ public sealed partial class SqlConnection : DbConnection, ICloneable
private string _connectionString;
private int _connectRetryCount;
private string _accessToken; // Access Token to be used for token based authentication
+ private SqlClientApp _sqlClientAppId = SqlClientApp.Unknown; // middleware application identity reported at login
// connection resiliency
private object _reconnectLock;
@@ -262,6 +263,7 @@ private SqlConnection(SqlConnection connection)
_accessToken = connection._accessToken;
_accessTokenCallback = connection._accessTokenCallback;
+ _sqlClientAppId = connection._sqlClientAppId;
// CopyFrom retains the source PoolGroup, and therefore the source ConnectionPoolKey.
// The provider must be copied along with it, otherwise the clone would authenticate
@@ -381,6 +383,32 @@ public static void RegisterColumnEncryptionKeyStoreProviders(IDictionary
+ public SqlClientApp SqlClientAppId
+ {
+ get => _sqlClientAppId;
+ set
+ {
+ // The identity is only reported while logging in, so allowing it
+ // to change afterwards would let the getter report a value that
+ // was never sent.
+ if (!InnerConnection.AllowSetConnectionString)
+ {
+ throw ADP.OpenConnectionPropertySet(nameof(SqlClientAppId), InnerConnection.State);
+ }
+
+ // Identifiers are carried in 16 bits, so anything outside that
+ // range cannot be reported and is rejected here rather than
+ // being silently truncated at login.
+ if ((int)value < 0 || (int)value > ushort.MaxValue)
+ {
+ throw SQL.InvalidSqlClientAppId(value, nameof(value));
+ }
+
+ _sqlClientAppId = value;
+ }
+ }
+
///
public void RegisterColumnEncryptionKeyStoreProvidersOnConnection(IDictionary customProviders)
{
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs
index c3b7bfd13b..4bb1abd3b4 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs
@@ -712,7 +712,8 @@ protected virtual DbConnectionInternal CreateConnection(
redirectedUserInstance: false,
applyTransientFaultHandling: applyTransientFaultHandling,
sspiContextProvider: key.SspiContextProvider,
- metrics: Metrics);
+ metrics: Metrics,
+ sqlClientAppId: sqlOwningConnection?.SqlClientAppId ?? SqlClientApp.Unknown);
using (sseConnection)
{
// NOTE: Retrieve here. This user instance name will be
@@ -774,7 +775,8 @@ protected virtual DbConnectionInternal CreateConnection(
pool,
key.AccessTokenCallback,
key.SspiContextProvider,
- metrics: Metrics);
+ metrics: Metrics,
+ sqlClientAppId: sqlOwningConnection?.SqlClientAppId ?? SqlClientApp.Unknown);
}
private static DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions(SqlConnectionOptions connectionOptions)
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs
index c9388a42f1..5e0afa47ce 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs
@@ -1856,6 +1856,15 @@ internal static Exception EmptyProviderName()
#endregion Always Encrypted Errors
+ internal static Exception InvalidSqlClientAppId(SqlClientApp value, string parameterName)
+ {
+ return ADP.ArgumentOutOfRange(
+ StringsHelper.GetString(
+ Strings.SQL_InvalidSqlClientAppId,
+ ((int)value).ToString(CultureInfo.InvariantCulture)),
+ parameterName);
+ }
+
//
// Merged Provider
//
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 d8c904f1e0..7c079cfee0 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs
@@ -1356,12 +1356,15 @@ internal void TdsLogin(
}
int feOffset = length;
+ // Capture the payload once so the length reserved below and the
+ // bytes written by WriteLoginData can never disagree.
+ ReadOnlyMemory userAgent = UserAgent.GetUcs2Bytes(rec.appId);
// calculate and reserve the required bytes for the featureEx
length = ApplyFeatureExData(
requestedFeatures,
recoverySessionData,
fedAuthFeatureExtensionData,
- UserAgent.Ucs2Bytes,
+ userAgent,
useFeatureExt,
length
);
@@ -1380,7 +1383,8 @@ internal void TdsLogin(
length,
feOffset,
clientInterfaceName,
- sspiWriter is { } ? sspiWriter.WrittenSpan : ReadOnlySpan.Empty);
+ sspiWriter is { } ? sspiWriter.WrittenSpan : ReadOnlySpan.Empty,
+ userAgent);
}
finally
{
@@ -9261,7 +9265,8 @@ private void WriteLoginData(SqlLogin rec,
int length,
int featureExOffset,
string clientInterfaceName,
- ReadOnlySpan outSSPI)
+ ReadOnlySpan outSSPI,
+ ReadOnlyMemory userAgent)
{
try
{
@@ -9521,7 +9526,7 @@ private void WriteLoginData(SqlLogin rec,
requestedFeatures,
recoverySessionData,
fedAuthFeatureExtensionData,
- UserAgent.Ucs2Bytes,
+ userAgent,
useFeatureExt,
length,
true
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs
index 6bcc3f2d41..80b832f91d 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs
@@ -124,6 +124,7 @@ internal sealed class SqlLogin
internal bool readOnlyIntent = false; // read-only intent
internal SqlCredential credential; // user id and password in SecureString
internal SecureString newSecurePassword;
+ internal SqlClientApp appId = SqlClientApp.Unknown; // middleware application identity
}
#nullable enable
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent.cs
index d8820c3ce9..7d338a1bde 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent.cs
@@ -3,8 +3,11 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Diagnostics;
+using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
+using System.Threading;
#nullable enable
@@ -31,9 +34,15 @@ internal static class UserAgent
/// never larger than 256 characters.
///
///
- /// The format is pipe ('|') delimited into 7 parts:
+ /// The format is pipe ('|') delimited into 9 parts:
///
- /// 1|MS-MDS|{Driver Version}|{Arch}|{OS Type}|{OS Info}|{Runtime Info}
+ /// 2|MS-MDS|{Driver Version}|{Arch}|{OS Type}|{OS Info}|{Runtime Info}|{App Id}|{Driver Properties}
+ ///
+ ///
+ /// This is the base value, whose {App Id} part is always
+ /// 0000. The payload actually sent at login carries the
+ /// identifier set on the connection; see
+ /// GetUcs2Bytes.
///
///
/// The {Driver Version} part is the version of the driver,
@@ -76,6 +85,19 @@ internal static class UserAgent
/// Maximum length is 44 characters.
///
///
+ /// The {App Id} part is the identifier of the application
+ /// middleware using the driver, serialized as exactly four uppercase
+ /// hexadecimal characters, zero-padded. It is always present;
+ /// 0000 means no application identity was reported. Maximum
+ /// length is 4 characters.
+ ///
+ ///
+ /// The {Driver Properties} part is a driver-owned feature flag
+ /// value, serialized as exactly four uppercase hexadecimal characters,
+ /// zero-padded. It is always present. Maximum length is 4
+ /// characters.
+ ///
+ ///
/// Any characters from the sourced values that are not one of the
/// following are replaced with underscore ('_'):
///
@@ -108,6 +130,78 @@ internal static class UserAgent
#region Helpers
+ ///
+ ///
+ /// Returns the UCS-2 encoded payload reporting the given application
+ /// identifier.
+ ///
+ ///
+ /// When is
+ /// , is
+ /// returned, whose App Id part is 0000.
+ ///
+ ///
+ ///
+ /// The application identifier set on the connection being logged in.
+ ///
+ /// The UCS-2 encoded payload bytes.
+ internal static ReadOnlyMemory GetUcs2Bytes(SqlClientApp app)
+ {
+ if (app == SqlClientApp.Unknown)
+ {
+ return Ucs2Bytes;
+ }
+
+ // Most processes report a single application identifier, so a single
+ // cached entry serves every login. The pair is cached behind one
+ // reference so readers never observe a torn ReadOnlyMemory.
+ AppPayload? cached = Volatile.Read(ref s_appPayload);
+ if (cached is not null && cached.App == app)
+ {
+ return cached.Ucs2Bytes;
+ }
+
+ ReadOnlyMemory bytes = Encoding.Unicode.GetBytes(BuildPayload(app));
+ Volatile.Write(ref s_appPayload, new AppPayload(app, bytes));
+
+ return bytes;
+ }
+
+ ///
+ /// Build the payload string from the current runtime environment,
+ /// reporting the given application identifier.
+ ///
+ /// The application identifier to report.
+ /// The payload string value.
+ private static string BuildPayload(SqlClientApp app) =>
+ Build(
+ MaxLenOverall,
+ PayloadVersion,
+ DriverName,
+ ThisAssembly.PackageVersion,
+ RuntimeInformation.ProcessArchitecture,
+ s_osType,
+ RuntimeInformation.OSDescription,
+ RuntimeInformation.FrameworkDescription,
+ ToAppId(app),
+ (ushort)SqlClientDriverPropertiesResolver.Current);
+
+ ///
+ /// Narrow an application identifier to the 16 bits the payload reports.
+ ///
+ ///
+ /// rejects values outside the
+ /// 16-bit range, so this conversion is always lossless.
+ ///
+ /// The application identifier to narrow.
+ /// The narrowed application identifier.
+ private static ushort ToAppId(SqlClientApp app)
+ {
+ Debug.Assert((int)app >= 0 && (int)app <= ushort.MaxValue);
+
+ return (ushort)app;
+ }
+
///
/// Static construction builds the Client Interface Name. All known
/// exceptions are consumed.
@@ -139,16 +233,11 @@ static UserAgent()
}
#endif
+ // Remember it for agent payloads built later.
+ s_osType = osType;
+
// Build it!
- Value = Build(
- MaxLenOverall,
- PayloadVersion,
- DriverName,
- ThisAssembly.PackageVersion,
- RuntimeInformation.ProcessArchitecture,
- osType,
- RuntimeInformation.OSDescription,
- RuntimeInformation.FrameworkDescription);
+ Value = BuildPayload(SqlClientApp.Unknown);
// Convert it to UCS-2 bytes.
//
@@ -189,6 +278,14 @@ static UserAgent()
///
/// The value of the Runtime Info part.
///
+ ///
+ /// The value of the App Id part, serialized as four uppercase
+ /// hexadecimal characters.
+ ///
+ ///
+ /// The value of the Driver Properties part, serialized as four uppercase
+ /// hexadecimal characters.
+ ///
///
/// The payload string value, never null, never empty, and never longer
/// than .
@@ -201,7 +298,9 @@ internal static string Build(
Architecture arch,
string osType,
string osInfo,
- string runtimeInfo)
+ string runtimeInfo,
+ ushort appId = 0,
+ ushort driverProperties = 0)
{
string result;
@@ -244,6 +343,19 @@ internal static string Build(
// Add the Runtime Info, truncating to its max length.
name.Append(Truncate(Clean(runtimeInfo), MaxLenRuntimeInfo));
+ name.Append('|');
+
+ // Add the App Id. It is fixed-width hexadecimal, so it can never
+ // exceed its maximum length and needs no cleaning.
+ string appIdPart = FormatHex(appId);
+ Debug.Assert(appIdPart.Length == MaxLenAppId);
+ name.Append(appIdPart);
+ name.Append('|');
+
+ // Add the Driver Properties, on the same terms as the App Id.
+ string driverPropertiesPart = FormatHex(driverProperties);
+ Debug.Assert(driverPropertiesPart.Length == MaxLenDriverProperties);
+ name.Append(driverPropertiesPart);
// Remember the name we've built up.
result = name.ToString();
@@ -254,7 +366,8 @@ internal static string Build(
// value.
result =
$"{payloadVersion}|{driverName}|{Unknown}|{Unknown}|" +
- $"{Unknown}|{Unknown}|{Unknown}";
+ $"{Unknown}|{Unknown}|{Unknown}|{FormatHex(appId)}|" +
+ $"{FormatHex(driverProperties)}";
}
// Truncate to our max length if necessary.
@@ -371,6 +484,20 @@ internal static string Clean(string? value)
}
}
+ ///
+ /// Format the given value as exactly four uppercase hexadecimal
+ /// characters, zero-padded.
+ ///
+ ///
+ /// A never needs more than four hexadecimal
+ /// characters, so the result is always exactly
+ /// characters and can never be truncated.
+ ///
+ /// The value to format.
+ /// The formatted value.
+ internal static string FormatHex(ushort value) =>
+ value.ToString("X4", CultureInfo.InvariantCulture);
+
///
/// Truncate the given value to the given max length, and return the
/// result.
@@ -398,7 +525,9 @@ internal static string Truncate(string value, ushort maxLength)
#region Private Fields
// Our payload format version.
- private const string PayloadVersion = "1";
+ //
+ // Version 2 adds the App Id and Driver Properties parts.
+ private const string PayloadVersion = "2";
// Our well-known .NET driver name.
private const string DriverName = "MS-MDS";
@@ -414,6 +543,8 @@ internal static string Truncate(string value, ushort maxLength)
private const ushort MaxLenOsType = 10;
private const ushort MaxLenOsInfo = 44;
private const ushort MaxLenRuntimeInfo = 44;
+ private const ushort MaxLenAppId = 4;
+ private const ushort MaxLenDriverProperties = 4;
// The OS Type values we promise in our API.
private const string Windows = "Windows";
@@ -428,5 +559,29 @@ internal static string Truncate(string value, ushort maxLength)
// unknown, invalid, or when errors occur.
private const string Unknown = "Unknown";
+ // The OS Type resolved during static construction, retained so payloads
+ // built later use the same value as Value.
+ private static readonly string s_osType;
+
+ // The payload for the most recently requested application identifier,
+ // built on first use.
+ private static AppPayload? s_appPayload;
+
+ ///
+ /// Pairs a payload with the application identifier it was built for.
+ ///
+ private sealed class AppPayload
+ {
+ internal AppPayload(SqlClientApp app, ReadOnlyMemory ucs2Bytes)
+ {
+ App = app;
+ Ucs2Bytes = ucs2Bytes;
+ }
+
+ internal SqlClientApp App { get; }
+
+ internal ReadOnlyMemory Ucs2Bytes { get; }
+ }
+
#endregion Private Fields
}
diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs
index c8f18d38bc..cff74a5ce5 100644
--- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs
+++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs
@@ -2814,6 +2814,15 @@ internal static string SQL_ActiveDirectoryInvalidStateTransition {
}
}
+ ///
+ /// Looks up a localized string similar to The SqlClient application identifier '{0}' is outside the supported range of 0 to 65535..
+ ///
+ internal static string SQL_InvalidSqlClientAppId {
+ get {
+ return ResourceManager.GetString("SQL_InvalidSqlClientAppId", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Unsupported state: '{0}'..
///
diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx
index 57cbf80016..05e7ed5491 100644
--- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx
+++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx
@@ -2199,6 +2199,9 @@
Cannot transition from state '{0}' to '{1}'.
+
+ The SqlClient application identifier '{0}' is outside the supported range of 0 to 65535.
+
Unsupported state: '{0}'.
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs
index 830f1bb733..7be39757a2 100644
--- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs
@@ -329,7 +329,13 @@ private static void CancelAndDisposePreparedCommand(string constr)
try
{
// Generate a query with a large number of results.
- using (var command = new SqlCommand("select @P from sys.objects a cross join sys.objects b cross join sys.objects c cross join sys.objects d cross join sys.objects e cross join sys.objects f", connection))
+ // The rows come from constant row sets rather than the system
+ // catalog, so the scan cannot contend with concurrent DDL on
+ // the shared test database.
+ const string rows = "(values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12),(13),(14),(15))";
+ string sql = $"select @P from {rows} a(n) cross join {rows} b(n) cross join {rows} c(n) " +
+ $"cross join {rows} d(n) cross join {rows} e(n) cross join {rows} f(n)";
+ using (var command = new SqlCommand(sql, connection))
{
command.Parameters.Add(new SqlParameter("@P", SqlDbType.Int) { Value = expectedValue });
connection.Open();
diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs
index 1a48ccd293..161dc6ae13 100644
--- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs
+++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs
@@ -25,6 +25,7 @@
namespace Microsoft.Data.SqlClient.UnitTests.SimulatedServerTests
{
+ [Collection(SimulatedServerTestCollection.Name)]
public class ConnectionTests
{
[Fact]
@@ -1170,9 +1171,10 @@ public void TestConnWithVectorFeatExtVersionNegotiation(bool expectedConnectionR
}
}
- // Test that the driver sends the UserAgent feature extension when
- // the context switch is enabled, and that the presence or absence of
- // an ack from the server has no effect.
+ ///
+ /// Verifies that LOGIN7 sends the USERAGENT payload carrying the connection's application
+ /// identity, regardless of whether the server acknowledges the extension.
+ ///
[Theory]
// Allow the server to ack.
[InlineData(true)]
@@ -1227,6 +1229,7 @@ public void TestConnWithUserAgentFeatureExtension(bool sendAck)
}.ConnectionString;
using var connection = new SqlConnection(connStr);
+ connection.SqlClientAppId = SqlClientApp.EntityFramework;
connection.Open();
// Verify the connection itself succeeded
@@ -1237,9 +1240,38 @@ public void TestConnWithUserAgentFeatureExtension(bool sendAck)
Assert.True(firstFeatureIsUserAgent);
Assert.True(tokenWasNotNull);
Assert.True(dataLengthAtLeast1);
- Assert.Equal(UserAgent.Ucs2Bytes.ToArray(), observedPayload);
+ Assert.Equal(UserAgent.GetUcs2Bytes(SqlClientApp.EntityFramework).ToArray(), observedPayload);
// TODO: Confirm the server sent an Ack by reading log message from SqlInternalConnectionTds
}
+
+ ///
+ /// Verifies the application identity cannot be changed once the connection is open, since
+ /// it is only reported during login and the getter would otherwise report a value that was
+ /// never sent.
+ ///
+ [Fact]
+ public void SqlClientAppId_CannotBeSet_WhenConnectionIsOpen()
+ {
+ using TdsServer server = new();
+ server.Start();
+
+ var connStr = new SqlConnectionStringBuilder
+ {
+ DataSource = $"localhost,{server.EndPoint.Port}",
+ Encrypt = SqlConnectionEncryptOption.Optional,
+ Pooling = false,
+ }.ConnectionString;
+
+ using var connection = new SqlConnection(connStr);
+ connection.SqlClientAppId = SqlClientApp.EntityFramework;
+ connection.Open();
+
+ Assert.Throws(
+ () => connection.SqlClientAppId = SqlClientApp.SemanticKernel);
+
+ // The connection still reports the identity it logged in with.
+ Assert.Equal(SqlClientApp.EntityFramework, connection.SqlClientAppId);
+ }
}
}
diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SqlClientAppTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SqlClientAppTests.cs
new file mode 100644
index 0000000000..fd14c57fba
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SqlClientAppTests.cs
@@ -0,0 +1,172 @@
+// 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 Microsoft.Data.SqlClient;
+using Xunit;
+
+namespace Microsoft.Data.SqlClient.UnitTests;
+
+///
+/// Tests for the application identifier registry.
+///
+public class SqlClientAppTests
+{
+ ///
+ /// Verifies the enum is CLS-compliant, so it is usable from every .NET
+ /// language.
+ ///
+ [Fact]
+ public void UnderlyingType_Is_Int()
+ {
+ Assert.Equal(typeof(int), Enum.GetUnderlyingType(typeof(SqlClientApp)));
+ }
+
+ ///
+ /// Verifies the default value reports no application identity.
+ ///
+ [Fact]
+ public void Default_Is_Unknown()
+ {
+ Assert.Equal(SqlClientApp.Unknown, default(SqlClientApp));
+ Assert.Equal(0, (int)SqlClientApp.Unknown);
+ }
+
+ ///
+ /// Verifies the reserved identifiers keep their assigned values, since
+ /// changing one would silently re-map an application's telemetry.
+ ///
+ [Theory]
+ [InlineData(SqlClientApp.EntityFramework, 0x0001)]
+ [InlineData(SqlClientApp.SemanticKernel, 0x0002)]
+ [InlineData(SqlClientApp.ManagementStudio, 0x0003)]
+ [InlineData(SqlClientApp.SqlManagementObjects, 0x0004)]
+ [InlineData(SqlClientApp.DataTierApplicationFramework, 0x0005)]
+ [InlineData(SqlClientApp.SqlToolsService, 0x0006)]
+ [InlineData(SqlClientApp.AspNetCoreDistributedSqlServerCache, 0x0007)]
+ [InlineData(SqlClientApp.EntityFramework6, 0x0008)]
+ [InlineData(SqlClientApp.AzureFunctionsSqlExtension, 0x0009)]
+ [InlineData(SqlClientApp.OrleansAdoNet, 0x000A)]
+ [InlineData(SqlClientApp.DurableTaskSqlServer, 0x000B)]
+ [InlineData(SqlClientApp.SqlPackage, 0x000C)]
+ public void Members_Have_Stable_Values(SqlClientApp app, int expected)
+ {
+ Assert.Equal(expected, (int)app);
+ }
+
+ ///
+ /// Verifies an unregistered identifier can be reported by casting, which
+ /// keeps the API forward compatible with identifiers added later.
+ ///
+ [Fact]
+ public void Unregistered_Identifier_Is_Accepted()
+ {
+ SqlClientApp app = (SqlClientApp)0xC001;
+
+ Assert.False(Enum.IsDefined(typeof(SqlClientApp), app));
+
+ using SqlConnection connection = new();
+ connection.SqlClientAppId = app;
+
+ Assert.Equal(app, connection.SqlClientAppId);
+ }
+
+ ///
+ /// Verifies the boundaries of the 16-bit identifier space are accepted,
+ /// since the payload reports the identifier in exactly 16 bits.
+ ///
+ [Theory]
+ [InlineData(0)]
+ [InlineData(ushort.MaxValue)]
+ public void Identifier_In_Range_Is_Accepted(int value)
+ {
+ using SqlConnection connection = new();
+
+ connection.SqlClientAppId = (SqlClientApp)value;
+
+ Assert.Equal(value, (int)connection.SqlClientAppId);
+ }
+
+ ///
+ /// Verifies an identifier outside the 16-bit space is rejected rather than
+ /// silently truncated when the payload is built.
+ ///
+ [Theory]
+ [InlineData(-1)]
+ [InlineData(ushort.MaxValue + 1)]
+ [InlineData(int.MaxValue)]
+ [InlineData(int.MinValue)]
+ public void Identifier_Out_Of_Range_Throws(int value)
+ {
+ using SqlConnection connection = new();
+
+ Assert.Throws(
+ () => connection.SqlClientAppId = (SqlClientApp)value);
+
+ // The rejected value is not retained.
+ Assert.Equal(SqlClientApp.Unknown, connection.SqlClientAppId);
+ }
+
+ ///
+ /// Verifies the connection reports no application identity until one is
+ /// assigned, and round-trips the value it is given.
+ ///
+ [Fact]
+ public void SqlConnection_SqlClientAppId_RoundTrips()
+ {
+ using SqlConnection connection = new();
+
+ Assert.Equal(SqlClientApp.Unknown, connection.SqlClientAppId);
+
+ connection.SqlClientAppId = SqlClientApp.SemanticKernel;
+
+ Assert.Equal(SqlClientApp.SemanticKernel, connection.SqlClientAppId);
+ }
+
+ ///
+ /// Verifies a cloned connection keeps the application identity of the
+ /// connection it was cloned from, so cloning does not silently drop the
+ /// identity back to .
+ ///
+ [Fact]
+ public void Clone_Preserves_SqlClientAppId()
+ {
+ using SqlConnection connection = new();
+ connection.SqlClientAppId = SqlClientApp.SqlPackage;
+
+ using SqlConnection clone = (SqlConnection)((ICloneable)connection).Clone();
+
+ Assert.Equal(SqlClientApp.SqlPackage, clone.SqlClientAppId);
+ }
+
+ ///
+ /// Verifies the driver properties part reports the connection pool V2 flag
+ /// when, and only when, that implementation is enabled.
+ ///
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void DriverProperties_Reports_ConnectionPoolV2(bool useConnectionPoolV2)
+ {
+ SqlClientDriverProperties expected = useConnectionPoolV2
+ ? SqlClientDriverProperties.ConnectionPoolV2
+ : SqlClientDriverProperties.None;
+
+ Assert.Equal(expected, SqlClientDriverPropertiesResolver.Resolve(useConnectionPoolV2));
+ }
+
+ ///
+ /// Verifies the flags reported for this process agree with the switch they
+ /// are derived from, so
+ /// cannot drift from the mapping it delegates to.
+ ///
+ [Fact]
+ public void DriverProperties_Current_Matches_Switch()
+ {
+ SqlClientDriverProperties expected =
+ SqlClientDriverPropertiesResolver.Resolve(LocalAppContextSwitches.UseConnectionPoolV2);
+
+ Assert.Equal(expected, SqlClientDriverPropertiesResolver.Current);
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentTests.cs
index 80b826700e..322ed4691e 100644
--- a/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentTests.cs
+++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentTests.cs
@@ -72,11 +72,12 @@ public void Value_Runtime_Parts()
//
// The format should be:
//
- // 1|MS-MDS|{Driver Version}|{Arch}|{OS Type}|{OS Info}|{Runtime Info}
+ // 2|MS-MDS|{Driver Version}|{Arch}|{OS Type}|{OS Info}|{Runtime Info}|
+ // {App Id}|{Driver Properties}
//
var parts = value.Split('|');
- Assert.Equal(7, parts.Length);
- Assert.Equal("1", parts[0]);
+ Assert.Equal(9, parts.Length);
+ Assert.Equal("2", parts[0]);
Assert.Equal("MS-MDS", parts[1]);
Assert.Equal(ThisAssembly.PackageVersion, parts[2]);
@@ -116,6 +117,14 @@ public void Value_Runtime_Parts()
// Runtime Info must be non-empty and 44 characters or less.
Assert.True(parts[6] == "Unknown" || parts[6].Length > 0);
Assert.True(parts[6].Length <= 44);
+
+ // App Id defaults to Unknown, and is always four hexadecimal
+ // characters.
+ Assert.Equal("0000", parts[7]);
+
+ // Driver Properties is always four hexadecimal characters.
+ Assert.Equal(4, parts[8].Length);
+ Assert.Matches("^[0-9A-F]{4}$", parts[8]);
}
///
@@ -149,6 +158,96 @@ public void Ucs2Bytes_Runtime_Parts()
Assert.Equal(UserAgent.Value, value);
}
+ ///
+ /// Test that the default payload is reused when no application identity is
+ /// set on the connection.
+ ///
+ [Fact]
+ public void GetUcs2Bytes_Unknown_App_Returns_Value()
+ {
+ var bytes = UserAgent.GetUcs2Bytes(SqlClientApp.Unknown);
+
+ Assert.Equal(UserAgent.Ucs2Bytes.ToArray(), bytes.ToArray());
+ Assert.Equal(9, Decode(bytes).Split('|').Length);
+ }
+
+ ///
+ /// Test that an application identity is reported in the App Id part,
+ /// leaving the other parts unchanged.
+ ///
+ [Fact]
+ public void GetUcs2Bytes_App_Sets_App_Id()
+ {
+ string value = Decode(UserAgent.GetUcs2Bytes(SqlClientApp.SemanticKernel));
+
+ _output.WriteLine($"UserAgent with app: {value}");
+
+ var parts = value.Split('|');
+ Assert.Equal(9, parts.Length);
+ Assert.Equal("0002", parts[7]);
+
+ // Every other part matches the default payload.
+ var defaultParts = UserAgent.Value.Split('|');
+ for (int i = 0; i < parts.Length; ++i)
+ {
+ if (i != 7)
+ {
+ Assert.Equal(defaultParts[i], parts[i]);
+ }
+ }
+ }
+
+ ///
+ /// Test that the payload for an application identity is built once and
+ /// reused across logins.
+ ///
+ [Fact]
+ public void GetUcs2Bytes_App_Reuses_Payload()
+ {
+ Assert.True(
+ UserAgent.GetUcs2Bytes(SqlClientApp.ManagementStudio).Span.Overlaps(
+ UserAgent.GetUcs2Bytes(SqlClientApp.ManagementStudio).Span));
+ }
+
+ ///
+ /// Test that the Build() function emits the App Id and Driver Properties as
+ /// four uppercase hexadecimal characters.
+ ///
+ [Theory]
+ [InlineData((ushort)0, (ushort)0, "2|A|B|X64|C|D|E|0000|0000")]
+ [InlineData((ushort)7, (ushort)1, "2|A|B|X64|C|D|E|0007|0001")]
+ [InlineData((ushort)0x00AB, (ushort)0, "2|A|B|X64|C|D|E|00AB|0000")]
+ [InlineData(ushort.MaxValue, ushort.MaxValue, "2|A|B|X64|C|D|E|FFFF|FFFF")]
+ public void Build_App_Id_And_Driver_Properties(
+ ushort appId,
+ ushort driverProperties,
+ string expected)
+ {
+ Assert.Equal(
+ expected,
+ UserAgent.Build(
+ maxLen: 256,
+ payloadVersion: "2",
+ driverName: "A",
+ driverVersion: "B",
+ Architecture.X64,
+ osType: "C",
+ osInfo: "D",
+ runtimeInfo: "E",
+ appId: appId,
+ driverProperties: driverProperties));
+ }
+
+ ///
+ /// Decode a UCS-2 encoded payload back to its string form.
+ ///
+ private static string Decode(ReadOnlyMemory bytes) =>
+ #if NET
+ Encoding.Unicode.GetString(bytes.Span);
+ #else
+ Encoding.Unicode.GetString(bytes.ToArray());
+ #endif
+
///
/// Test the Build() function when it truncates the overall length.
///
@@ -171,6 +270,9 @@ public void Ucs2Bytes_Runtime_Parts()
[InlineData(13, "2|A|B|X64|C|D")]
[InlineData(14, "2|A|B|X64|C|D|")]
[InlineData(15, "2|A|B|X64|C|D|E")]
+ [InlineData(16, "2|A|B|X64|C|D|E|")]
+ [InlineData(20, "2|A|B|X64|C|D|E|0000")]
+ [InlineData(25, "2|A|B|X64|C|D|E|0000|0000")]
public void Build_Truncate_Overall(ushort maxLen, string expected)
{
Assert.Equal(
@@ -200,7 +302,7 @@ public void Build_Truncate_Payload_Version()
// The payload version is longer than its per-field max length of 2.
Assert.Equal(
- "12|A|B|X64|C|D|E",
+ "12|A|B|X64|C|D|E|0000|0000",
UserAgent.Build(
128, "1234", "A", "B", Architecture.X64, "C", "D", "E"));
}
@@ -219,7 +321,7 @@ public void Build_Truncate_Driver_Name()
// The driver name is longer than its per-field max length of 12.
Assert.Equal(
- "2|LongDriverNa|B|X64|C|D|E",
+ "2|LongDriverNa|B|X64|C|D|E|0000|0000",
UserAgent.Build(
128, "2", "LongDriverName", "B", Architecture.X64, "C",
"D", "E"));
@@ -240,7 +342,7 @@ public void Build_Truncate_Driver_Version()
// The driver version is longer than its per-field max length of 24.
Assert.Equal(
- "2|A|ReallyLongDriverVersionS|X64|C|D|E",
+ "2|A|ReallyLongDriverVersionS|X64|C|D|E|0000|0000",
UserAgent.Build(
128, "2", "A", "ReallyLongDriverVersionString",
Architecture.X64, "C", "D", "E"));
@@ -264,7 +366,7 @@ public void Build_Truncate_Arch()
#if NET
// The Architecture is longer than its per-field max length of 10.
Assert.Equal(
- "2|A|B|LoongArch6|C|D|E",
+ "2|A|B|LoongArch6|C|D|E|0000|0000",
UserAgent.Build(
128, "2", "A", "B", Architecture.LoongArch64, "C", "D", "E"));
#endif
@@ -284,7 +386,7 @@ public void Build_Truncate_OS_Type()
// The OS Type is longer than its per-field max length of 10.
Assert.Equal(
- "2|A|B|X64|VeryLongOs|D|E",
+ "2|A|B|X64|VeryLongOs|D|E|0000|0000",
UserAgent.Build(
128, "2", "A", "B", Architecture.X64, "VeryLongOsName", "D",
"E"));
@@ -304,7 +406,7 @@ public void Build_Truncate_OS_Info()
// The OS Type is longer than its per-field max length of 44.
Assert.Equal(
- "2|A|B|X64|C|01234567890123456789012345678901234567890123|E",
+ "2|A|B|X64|C|01234567890123456789012345678901234567890123|E|0000|0000",
UserAgent.Build(
128, "2", "A", "B", Architecture.X64, "C",
"01234567890123456789012345678901234567890123456789",
@@ -326,7 +428,7 @@ public void Build_Truncate_Runtime_Info()
// The Runtime Type is longer than its per-field max length of 44.
Assert.Equal(
- "2|A|B|X64|C|D|01234567890123456789012345678901234567890123",
+ "2|A|B|X64|C|D|01234567890123456789012345678901234567890123|0000|0000",
UserAgent.Build(
128, "2", "A", "B", Architecture.X64, "C", "D",
"01234567890123456789012345678901234567890123456789"));
@@ -357,7 +459,7 @@ public void Build_Truncate_Most()
"D01234567890123456789012345678901234567890123456789",
// Runtime Info > 44 chars.
"E01234567890123456789012345678901234567890123456789");
- Assert.Equal(145, name.Length);
+ Assert.Equal(155, name.Length);
Assert.Equal(
"12|" +
"A01234567890|" +
@@ -365,7 +467,8 @@ public void Build_Truncate_Most()
"X64|" +
"C012345678|" +
"D0123456789012345678901234567890123456789012|" +
- "E0123456789012345678901234567890123456789012",
+ "E0123456789012345678901234567890123456789012|" +
+ "0000|0000",
name);
}
@@ -396,7 +499,7 @@ public void Build_Truncate_All()
"D01234567890123456789012345678901234567890123456789",
// Runtime Info > 44 chars.
"E01234567890123456789012345678901234567890123456789");
- Assert.Equal(152, name.Length);
+ Assert.Equal(162, name.Length);
Assert.Equal(
"12|" +
"A01234567890|" +
@@ -404,7 +507,8 @@ public void Build_Truncate_All()
"LoongArch6|" +
"C012345678|" +
"D0123456789012345678901234567890123456789012|" +
- "E0123456789012345678901234567890123456789012",
+ "E0123456789012345678901234567890123456789012|" +
+ "0000|0000",
name);
}
#endif