Add SqlClient agent identifier to USERAGENT payload - #4632
Add SqlClient agent identifier to USERAGENT payload#4632cheenamalhotra wants to merge 7 commits into
Conversation
Adds an optional agent identifier to the USERAGENT login feature extension so known middleware (EF Core, SSMS, DacFx, ...) can be told apart from direct SqlClient use. - New public `SqlClientAgent` enum and `SqlConnection.RegisterSqlClientAgent(SqlClientAgent)`. - Registration is process-wide and allowed once, so an application cannot overwrite or spoof an agent set by a library. - Can also be set from App.config via a `SqlClientAgent` section. - Payload format bumped to version 2; the agent id is appended as an optional 8th part only when registered. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: efbed43f-1014-45d7-a9f6-9f04711c281e
There was a problem hiding this comment.
🟡 Changes recommended
The LOGIN7 length race, enum validation, test isolation, and documentation issues remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds process-wide middleware identification to the USERAGENT login payload.
Changes:
- Adds
SqlClientAgentregistration and configuration APIs. - Extends USERAGENT v2 with an optional agent ID.
- Adds tests, documentation, and samples.
File summaries
| File | Description |
|---|---|
src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentTests.cs |
Tests payload versioning and agent encoding. |
src/Microsoft.Data.SqlClient/tests/UnitTests/SqlClientAgentTests.cs |
Tests identifiers and configuration parsing. |
src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs |
Verifies LOGIN7 agent transmission. |
src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlClientAgentConfigurationTests.cs |
Tests configuration precedence. |
src/Microsoft.Data.SqlClient/tests/FunctionalTests/app.config |
Registers the test agent. |
src/Microsoft.Data.SqlClient/src/Resources/Strings.resx |
Adds registration error messages. |
src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs |
Exposes generated resource accessors. |
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent.cs |
Builds and caches agent payloads. |
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs |
Writes agent payloads into LOGIN7. |
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs |
Creates agent-related exceptions. |
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs |
Adds the registration API. |
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientAgent.cs |
Defines agents and registration logic. |
src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs |
Updates the public API contract. |
doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml |
Documents registration behavior. |
doc/samples/SqlConnection_RegisterSqlClientAgent.cs |
Demonstrates middleware registration. |
Review details
Files not reviewed (1)
- src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs: Generated file
Suppressed comments (1)
src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlClientAgentConfigurationTests.cs:20
- The test intent is written as ordinary comments, but test methods require XML
<summary>documentation. Convert this explanation to an XML summary so the new test follows the test documentation contract.
[ConditionalFact(typeof(TestUtility), nameof(TestUtility.IsNetFramework))]
public void AppConfigAgent_PreventsProgrammaticRegistration()
- Files reviewed: 14/15 changed files
- Comments generated: 4
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Capture the USERAGENT payload once in SendPreLoginHandshake and pass it to WriteLoginData, so a concurrent registration cannot make the reserved feature length disagree with the bytes written. - Restrict RegisterSqlClientAgent to declared enum members. Undeclared numeric ids remain valid in config, where forward compatibility matters. - Serialize ConnectionTests via SimulatedServerTestCollection; it now mutates process-wide registration. - Add XML summary to SqlClientAgentConfigurationTests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: efbed43f-1014-45d7-a9f6-9f04711c281e
There was a problem hiding this comment.
🔵 Needs a closer look
Malformed App.config handling needs isolated regression coverage before approval.
Review details
Files not reviewed (1)
- src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs: Generated file
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientAgent.cs:198
- The new tests validate
Parsedirectly, but none exercises this catch throughLoadFromAppConfig. Consequently, the stated guarantee that an invalid or malformed application configuration cannot turn first use into aTypeInitializationExceptionhas no regression coverage. Add an isolated-process/AppDomain test with a badSqlClientAgentsection that triggers registration loading and verifies the failure is consumed.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent.cs:39
Valueis always built withagentId: null, so it never has the optional eighth part; onlyGetUcs2Bytescan return that transmitted form. Describing theValueproperty itself with the optional format makes its contract inconsistent with the implementation and the seven-part tests. Clarify that this is the base value and that the encoded login payload may append the agent ID.
/// The format is pipe ('|') delimited into 7 parts, plus an optional
/// 8th part:
///
/// <code>2|MS-MDS|{Driver Version}|{Arch}|{OS Type}|{OS Info}|{Runtime Info}[|{Agent Id}]</code>
src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlClientAgentConfigurationTests.cs:24
- Convert the preceding ordinary comment into an XML
<summary>for this test method. The repository's test documentation rules require behavior-focused XML summaries on every test method.
[ConditionalFact(typeof(TestUtility), nameof(TestUtility.IsNetFramework))]
public void AppConfigAgent_PreventsProgrammaticRegistration()
- Files reviewed: 14/15 changed files
- Comments generated: 0 new
- Review effort level: Balanced
- Change SqlClientAgent to int-backed so it needs no CLSCompliant attribute, which the notsupported assembly rejects (CS3021). Identifiers are still bounded to a positive 16-bit range. - Extract LoadAgent so the configuration failure paths are testable, and cover malformed config, invalid id, wrong section type, missing section, and a throwing loader. - Clarify that UserAgent.Value never carries the agent id; only the login payload does. - Convert the App.config test comment to an XML summary. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: efbed43f-1014-45d7-a9f6-9f04711c281e
There was a problem hiding this comment.
🟡 Changes recommended
The public enum’s underlying type and CLS annotations do not match the advertised 16-bit API contract.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Files not reviewed (1)
- src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs: Generated file
- Files reviewed: 14/15 changed files
- Comments generated: 1
- Review effort level: Balanced
- Remove the now-unnecessary CLSCompliant attribute from the implementation method. - Document the 16-bit identifier contract on the enum, since it is no longer implied by the underlying type. - Assert the underlying type is Int32 so the CLS-compliant surface cannot regress. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: efbed43f-1014-45d7-a9f6-9f04711c281e
There was a problem hiding this comment.
🔵 Needs a closer look
Public API, configuration, and wire-payload changes require final human review, and two documentation nits remain.
Review details
Files not reviewed (1)
- src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs: Generated file
Suppressed comments (2)
src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs:603
- The PR's API example still declares
SqlClientAgent : ushort, but this public surface (andUnderlyingType_IsInt32) intentionally publishes anInt32-backed enum. Please update the PR description to omit: ushortor use: int, so consumers are not given an API signature that differs from the assembly.
public enum SqlClientAgent
src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentTests.cs:221
- Document the
bytesparameter and return value for this new test helper. The repository's test documentation rules require XML<param>and<returns>elements for helper methods where applicable.
/// <summary>
/// Decode a UCS-2 encoded payload back to its string form.
/// </summary>
private static string Decode(ReadOnlyMemory<byte> bytes) =>
- Files reviewed: 14/15 changed files
- Comments generated: 0 new
- Review effort level: Balanced
doc/samples builds against the published Microsoft.Data.SqlClient package, so it cannot reference an API that has not shipped yet. Move the example into the XML docs alongside the existing App.config example and drop the compiled sample file. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: efbed43f-1014-45d7-a9f6-9f04711c281e
There was a problem hiding this comment.
🔵 Needs a closer look
The PR description incorrectly documents SqlClientAgent as having a ushort underlying type.
Review details
Files not reviewed (1)
- src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs: Generated file
Suppressed comments (1)
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientAgent.cs:23
- The PR's API example still advertises
public enum SqlClientAgent : ushort, while this declaration, the reference surface, and the new underlying-type test intentionally publishInt32. Because the enum's underlying type is observable, update the PR description to showpublic enum SqlClientAgent(or: int) and describe 16 bits as the validated identifier range rather than the underlying type.
public enum SqlClientAgent
- Files reviewed: 13/14 changed files
- Comments generated: 0 new
- Review effort level: Balanced
|
CI failure analysis — none of the 6 failing legs are caused by this change.
The test runs a 6-way cross join over
Why this change can't be the cause: it only appends an optional 8th part to the LOGIN7 USERAGENT payload. No manual test references Re-running to clear the flakes. |
|
/azp run |
|
Azure Pipelines: Successfully started running 2 pipeline(s). |
CancelAndDisposePreparedCommand runs a 6-way cross join over sys.objects purely to produce a large result set. The scan takes shared locks on the catalog, so a concurrent DDL from another test leg on the shared database can pick it as the deadlock victim. Read the catalog with NOLOCK. The statement stays a single prepared SELECT and returns the same rows, so the test still covers what it was written for (disposing a connection whose prepared command was cancelled mid-read). Pre-existing flake, not related to the USERAGENT change in this PR; it also fails the same way on #4630. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: efbed43f-1014-45d7-a9f6-9f04711c281e
|
Re-running did not clear the The test cross joins This is unrelated to the USERAGENT change — the same test fails identically on #4630. Happy to split it into its own PR if you would rather keep this one focused. |
There was a problem hiding this comment.
🟡 Changes recommended
The registration scope contradicts its contract, and the cancellation test remains vulnerable to concurrent DDL interactions.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Files not reviewed (1)
- src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs: Generated file
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientAgent.cs:79
- The registration is not actually process-wide: this static field exists once per loaded SqlClient assembly instance, so separate .NET
AssemblyLoadContexts (and .NET FrameworkAppDomains) can each register a different agent. That contradicts the public contract and the stated anti-overwrite behavior. Either weaken the contract consistently to assembly-load-context/AppDomain scope, or use genuinely process-shared coordination.
- Files reviewed: 14/15 changed files
- Comments generated: 1
- Review effort level: Balanced
|
The deadlock fix worked — all 7 The only remaining failure is Re-running to clear it. |
|
/azp run PR-SqlClient-Project |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
| Validate(id); | ||
| if (Interlocked.CompareExchange(ref s_agentId, (int)id, 0) != 0) | ||
| { | ||
| throw SQL.SqlClientAgentAlreadyRegistered(); |
There was a problem hiding this comment.
This could cause issues if applications mix multiple middleware. Each middleware would need to correctly catch the exception. What about returning a boolean that indicates if this register set successfully?
There was a problem hiding this comment.
Good point — changed in 9c7f4b0. RegisterSqlClientAgent now returns bool: true if the call registered the agent, false if one was already registered. The first registration still wins and still cannot be replaced, but a second middleware no longer faults the application, so nobody has to wrap the call in a try/catch.
Invalid identifiers still throw ArgumentOutOfRangeException — that is a programming error rather than a lost race, so it stays loud.
Dropped the now-unused SQL_SqlClientAgentAlreadyRegistered resource and added Register_ReportsWhetherItWon for the new contract.
|
Second re-run: same result. 204 checks pass; every failure is a Root cause from the setup log: The SQL Server container crashes and core-dumps inside the Lima VM on the macOS agent. No driver code runs, so this cannot be affected by the change. It moves around between runs — net9 legs on one run, net8 and net9 on the next — which is the signature of an unstable agent pool rather than a test defect. #4633 hit the same failure. I have re-run twice and it does not clear; it needs someone with infra access. Everything else is green, including all |
…l test RegisterSqlClientAgent now returns bool instead of throwing when an agent is already registered. An application can load several middleware libraries that each register themselves, and only the first can win; making the loser throw meant every middleware had to guard the call or risk faulting the host. The first registration still wins and still cannot be replaced. Invalid identifiers continue to throw ArgumentOutOfRangeException, since that is a programming error rather than a lost race. CancelAndDisposePreparedCommand now builds its large result set from constant row sets. NOLOCK still takes schema-stability locks and can additionally fail a scan with error 601 on concurrent catalog changes, so it did not decouple the test from DDL on the shared database. Constant row sets touch no catalog at all, and 16^6 rows keep enough data on the wire to cancel mid-read. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: efbed43f-1014-45d7-a9f6-9f04711c281e
saurabh500
left a comment
There was a problem hiding this comment.
Left some comments about the enum and API related questions.
| /// <see cref="int"/> so the enum stays CLS-compliant; the range is enforced | ||
| /// when an agent is registered. | ||
| /// </remarks> | ||
| public enum SqlClientAgent |
There was a problem hiding this comment.
Why call it SqlClientAgent? Could this be AppIdentifier?
There was a problem hiding this comment.
Did we do an API discussion on the naming?
| /// <summary>Microsoft Azure Functions SQL extension.</summary> | ||
| AzureFunctionsSqlExtension = 9, | ||
|
|
||
| /// <summary>Microsoft Orleans ADO.NET providers.</summary> |
There was a problem hiding this comment.
What are these and do we have an agreement with them to use this capability?
| OrleansAdoNet = 10, | ||
|
|
||
| /// <summary>Microsoft Durable Task SQL Server provider.</summary> | ||
| DurableTaskSqlServer = 11 |
There was a problem hiding this comment.
There should be an Other with a high value which allows other enum variants to be injected into this list. This enum seems like first class treatment to a handful of Microsoft customers?
There was a problem hiding this comment.
🔵 Needs a closer look
The public documentation needs a security warning, and the PR description must accurately reflect the published API contract.
Review details
Files not reviewed (1)
- src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs: Generated file
Suppressed comments (3)
doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml:2328
- The public API documentation omits the PR's important constraint that this identifier is client-supplied telemetry and must not be trusted for security decisions. Because any application can call this public method (and can select any declared agent), add that warning explicitly to prevent downstream consumers from treating the value as authenticated identity.
<remarks>
<para>
This API is intended only for approved middleware partners. Applications should not call it directly.
</para>
src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs:1048
- The PR description still advertises a
ushort-backed enum and avoidregistration method whose later calls throw, but the published surface is now anint-backed enum with aboolmethod that returnsfalseon later calls. Please update the API block and registration semantics in the PR description so consumers and release-note authors see the actual contract.
/// <include file='../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/RegisterSqlClientAgent/*' />
public static bool RegisterSqlClientAgent(Microsoft.Data.SqlClient.SqlClientAgent id) { throw null; }
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs:386
- The PR description still advertises
public enum SqlClientAgent : ushort, avoidregistration method, andInvalidOperationExceptionon later registration, while the published API is now anint-backed enum and this method returnsfalse. Please update the description/API example and registration semantics so consumers do not review or adopt the wrong public contract.
public static bool RegisterSqlClientAgent(SqlClientAgent id)
=> SqlClientAgentRegistration.Register(id);
- Files reviewed: 14/15 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Fixes #3201
Summary
Adds an optional agent identifier to the USERAGENT login feature extension so known middleware (EF Core, SSMS, DacFx, Semantic Kernel, ...) can be distinguished from direct SqlClient use.
The identifier is a value from a closed enum, not a free-form string, so applications cannot inject arbitrary text into the telemetry payload.
API
Registration is process-wide and allowed once. A library registers itself at startup; a later call throws
InvalidOperationException, so an application cannot overwrite or spoof another component's agent.It can also be set from App.config, so the value can be fixed by deployment rather than code:
The config file wins over programmatic registration.
Payload
Format version bumped
1->2. The agent id is appended as an optional 8th part, only when registered:With no agent registered the payload is unchanged apart from the version part.
Numeric ids are accepted in config even if not yet in the enum, so an agent assigned an id after a driver release can still be configured.
Notes
Checklist
The net462 App.config test cannot run on macOS - needs CI validation.