-
Notifications
You must be signed in to change notification settings - Fork 311
Add Json Payload Functionality for User Agent Feature Extension #3489
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements the JSON payload functionality for the User Agent feature extension in Microsoft.Data.SqlClient, adding the core infrastructure for generating and managing user agent information that will be sent to SQL Server during connection establishment.
- Implements UserAgentInfo class with OS detection, architecture detection, and runtime information gathering
- Adds UserAgentInfoDto as a serializable data transfer object with size constraints and field-dropping logic
- Includes comprehensive unit tests covering field truncation, payload sizing, and JSON serialization
Reviewed Changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
File | Description |
---|---|
UserAgentInfo.cs (netfx/netcore) | Core implementation with OS/runtime detection and JSON payload generation with size constraints |
UserAgentInfoDto.cs (netfx/netcore) | Data transfer object for JSON serialization with property name constants |
UserAgentInfoTests.cs | Unit tests covering truncation logic, payload sizing, and JSON contract validation |
Microsoft.Data.SqlClient.csproj (netfx/netcore) | Project file updates to include the new UserAgent classes |
Microsoft.Data.SqlClient.UnitTests.csproj | Test project configuration with empty folder reference |
Comments suppressed due to low confidence (2)
src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj
Outdated
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/UserAgentInfo.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/UserAgentInfo.cs
Outdated
Show resolved
Hide resolved
@samsharma2700 @cheenamalhotra great to see this work happening. I've taken a look at the spec doc linked above, and could you clarify how we'd go about injecting EF information into the user agent? We'd need to identify EF Core (as opposed to direct users of SqlClient), it's version, etc. |
Hi @roji - There is a V2 of the design that adds a public API for middleware like EF to pass version information, but we need to go through a security and privacy review before we can implement it. This V1 phase only includes values we can pull from the runtime that don't have any such concerns. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to move the UserAgent files into the src/ project and avoid duplicating them. I'll complete my review after the move.
src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj
Outdated
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/UserAgentInfo.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/UserAgentInfo.cs
Outdated
Show resolved
Hide resolved
@paulmedynski ok - can you clarify what's planned for the 6.1.0 (v1, v2?) |
The V1 work will arrive in 7.0.0. The V2 work to support EF and other middleware may also make it into that timeframe, but depends on which reviews we need and how long they take. There is also ongoing SQL Server support that we need to align with to make any of this driver side work useful. @samsharma2700 will be completing the V1 work over several PRs. We have (and will) put them into the 7.0.0-preview1 milestone. |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3489 +/- ##
==========================================
- Coverage 68.85% 66.89% -1.97%
==========================================
Files 277 279 +2
Lines 62237 62316 +79
==========================================
- Hits 42854 41684 -1170
- Misses 19383 20632 +1249
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
@paulmedynski thanks for the clarifications! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're on the right track here. We can discuss my comments and suggestions.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very close now; just a few more comments.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs
Outdated
Show resolved
Hide resolved
// - If the payload exceeds 2,047 bytes but remains within sensible limits, we still send it, but note that | ||
// some servers may silently drop or reject such packets — behavior we may use for future probing or diagnostics. | ||
// - If payload exceeds 10KB even after dropping fields , we send an empty payload. | ||
var driverName = TruncateOrDefault(DriverName, DriverNameMaxChars); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can just assign directly to the DTO properties/fields instead of making temporaries here:
return new()
{
Driver = TruncateOrDefault(DriverName, DriverNameMaxChars).
...
}
You don't even need to repeat UserAgentInfoDto
since the compiler knows what the return type is.
|
||
// TODO: Does this need to be nullable? | ||
[JsonPropertyName(DriverJsonKey)] | ||
public string? Driver { get; set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These two should be non-nullable.
public const string DetailsJsonKey = "details"; | ||
|
||
[JsonPropertyName(TypeJsonKey)] | ||
public string? Type { get; set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't need to be nullable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do drop this field though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we drop the entire Os field, but never just the Type field.
// Convert to string for field presence checks | ||
string json = Encoding.UTF8.GetString(payload); | ||
// High‑priority fields must survive. | ||
Assert.True(root.TryGetProperty(UserAgentInfoDto.DriverJsonKey, out _)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can at least verify the driver name is "MS-MDS". You could also probably verify the version if you can access the ADP helper that returns it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this particular test all values are replaced as xxxx...
so driver name and version will not be the default values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way to test that you produce the expected real values?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a new test that verifies we populate the correct default values for driver name and version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can still verify that the present fields contain the expected 'xxx...' values, truncated to their respective max lengths.
{ | ||
Assert.Equal("{}", json.Trim()); | ||
return; | ||
Assert.True(os.TryGetProperty(UserAgentInfoDto.OsInfo.TypeJsonKey, out _)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should verify the value here, since we have a constrained set of allowable values and you can predict what they will be.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above, all values are still xxxx...
since we pass the DTO values directly and are only testing AdjustJsonPayloadSize
functionality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a new test to verify the field value handling.
This is a special case because UserAgentInfo
normally populates default values automatically. Here, we intentionally bypass UserAgentInfo
to create our own DTO
directly, without calling BuildDto()
or TruncateOrDefault()
.
As a result, no truncation of field values occurs. The main purpose of this test is to validate the field-dropping logic under these conditions.
} | ||
|
||
// 4. DTO serializes with expected JSON property names | ||
// 4. DTO JSON contract - verify names and values |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test serialization when the nullable fields are null.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😬 I'm gonna force braces for if statements if it's the last thing I do
|
||
#nullable enable | ||
|
||
#if WINDOWS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this actually supported in netfx?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should not by default, but need to remove this as we changed the implementation. Good catch!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're definitely going to have something like this once the common project is finished up. Didn't want you jumping the gun :)
/// <summary> | ||
/// Maximum number of characters allowed for the driver version. | ||
/// </summary> | ||
private const int VersionMaxChars = 16; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please order alphabetically
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it a new guideline we are following? Haven't seen alphabetical ordering in other classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These currently reflect the order of the JSON fields defined in the spec. I would prefer we retain that order throughout the implementation. Perhaps a comment explaining that here would help.
}, | ||
Arch = architecture, | ||
Runtime = runtime | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: extra line
{ | ||
// 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, sorry to do this but, please always always always use braces around if/else blocks
if (OperatingSystem.IsMacOS()) return OsType.macOS; | ||
#endif | ||
// second we fallback to OSPlatform checks | ||
#if NETCOREAPP3_0_OR_GREATER || NET5_0_OR_GREATER |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't support < net6, so these conditions seem unnecessary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do target net462
which does not support OSPlatform.FreeBSD
: https://apisof.net/catalog/138b234eefee5c6692118fe8c4d64920
Will change the check to reflect that.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfoDto.cs
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfoDto.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfoDto.cs
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfoDto.cs
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs
Show resolved
Hide resolved
|
||
// Assert – nested os object | ||
// optional Arch | ||
if (dto.Arch is null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my opinion, this test is doing too many scenarios...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand the test may feel tedious. I’ve added additional comments to improve readability.
This section needs to be tested in one run to properly verify the field-dropping logic.
/azp run |
Azure Pipelines successfully started running 2 pipeline(s). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Getting closer!
/// </summary> | ||
private const int OsTypeMaxChars = 16; | ||
internal const int JsonPayloadMaxBytes = 2047; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yuck - now the JSON field constants are mixed in with other constants, and they're no longer in logical order.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah we have to decide between logical groupings or lexicographical ordering. The groupings are maintained in case of UserAgentInfoDto
private static readonly UserAgentInfoDto s_dto; | ||
private static readonly byte[] s_cachedPayload; | ||
|
||
public static byte[] CachedPayload => s_cachedPayload; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This public property deserves some documentation, most importantly that it doesn't calculate anything.
Can this be readonly?
Is CachedPayload
the best name? Maybe it's fine when you add documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will add some documentation here. We cannot mark properties as readonly
. In case of s_cachedPayload
despite it being readonly
someone can still modify it after initialization. I think CachedPayload
can be ReadOnlyMemory<byte>
, which would communicate the read-only intent at the API boundary.
} | ||
|
||
dto.OS = null; // drop OS entirely | ||
// Last attempt to send minimal payload driver + version only |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that we know driver + version cannot be larger than the max, pointing to the explanation in the BuildDto()
commentary.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs
Show resolved
Hide resolved
[JsonPropertyName(DriverJsonKey)] | ||
public string? Driver { get; set; } | ||
public string Driver { get; set; } = UserAgentInfo.DriverName; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These initialization values don't belong here. The DTO doesn't know anything about where its values come from. That's UserAgentInfo
's responsibility.
I think string.Empty
is the correct choice here.
// Convert to string for field presence checks | ||
string json = Encoding.UTF8.GetString(payload); | ||
// High‑priority fields must survive. | ||
Assert.True(root.TryGetProperty(UserAgentInfoDto.DriverJsonKey, out _)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can still verify that the present fields contain the expected 'xxx...' values, truncated to their respective max lengths.
Assert.False(root.TryGetProperty(UserAgentInfoDto.ArchJsonKey, out _)); | ||
Assert.False(root.TryGetProperty(UserAgentInfoDto.RuntimeJsonKey, out _)); | ||
|
||
// If the "os" object survived, only its "type" sub‑field may remain. | ||
// if OS block remains, only its Type sub-field may survive | ||
if (root.TryGetProperty(UserAgentInfoDto.OsJsonKey, out JsonElement os)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This if-statement allows the test to pass whether or not the OS block is present. So we're not really testing one of the adjust scenarios. Here's what we need:
- Keep this test, rename it to indicate that all low priority fields are stripped, and then replace this if-statement with
Assert.False(root.TryGetProperty(OsJsonKey))
. - Add a new test that checks the middle-ground adjusting, where only arch, runtime, and os.description are stripped (i.e. the return on UserAgentInfo line 115).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great catch! will do that.
{ | ||
Assert.Equal(dto.OS.Details, os.GetProperty(UserAgentInfoDto.OsInfo.DetailsJsonKey).GetString()); | ||
} | ||
} | ||
} | ||
|
||
// 5. End-to-end test that combines truncation, adjustment, and serialization |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this is the missing test I was referring to above, just not named/documented accurately.
@@ -0,0 +1,263 @@ | |||
using System.Text; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Include .NET headers
@@ -0,0 +1,49 @@ | |||
using System.Text.Json; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Include headers
@@ -0,0 +1,348 @@ | |||
using System; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Include headers
|
||
#nullable enable | ||
|
||
namespace Microsoft.Data.SqlClient.UserAgent; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For consistency in reading, keep curly brackets for namespace definition, as in other classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was feedback to move to file-scoped namespace declarations : #3489 (comment)
Description
Part 2 of UserAgent work. Previous PR: #3451
Testing
Builds are running and added unit tests to verify changes.