Skip to content

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

samsharma2700
Copy link
Contributor

Description

Part 2 of UserAgent work. Previous PR: #3451

Testing

Builds are running and added unit tests to verify changes.

@Copilot Copilot AI review requested due to automatic review settings July 17, 2025 20:41
@samsharma2700 samsharma2700 requested a review from a team as a code owner July 17, 2025 20:41
Copy link
Contributor

@Copilot Copilot AI left a 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)

@roji
Copy link
Member

roji commented Jul 17, 2025

@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.

@paulmedynski
Copy link
Contributor

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.

Copy link
Contributor

@paulmedynski paulmedynski left a 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.

@roji
Copy link
Member

roji commented Jul 18, 2025

@paulmedynski ok - can you clarify what's planned for the 6.1.0 (v1, v2?)

@paulmedynski paulmedynski added this to the 7.0-preview1 milestone Jul 18, 2025
@paulmedynski
Copy link
Contributor

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.

Copy link

codecov bot commented Jul 18, 2025

Codecov Report

Attention: Patch coverage is 51.90840% with 63 lines in your changes missing coverage. Please review.

Project coverage is 66.89%. Comparing base (42146a3) to head (7b2ad0e).
Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
...icrosoft/Data/SqlClient/UserAgent/UserAgentInfo.cs 49.19% 63 Missing ⚠️
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     
Flag Coverage Δ
addons 91.04% <ø> (+0.22%) ⬆️
netcore 68.96% <50.38%> (-3.72%) ⬇️
netfx 69.34% <51.58%> (+1.15%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@roji
Copy link
Member

roji commented Jul 19, 2025

@paulmedynski thanks for the clarifications!

Copy link
Contributor

@paulmedynski paulmedynski left a 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.

Copy link
Contributor

@paulmedynski paulmedynski left a 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.

_cachedPayload = AdjustJsonPayloadSize(_dto);
}

static UserAgentInfoDto BuildDto()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have internal visibility.

// - 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);
Copy link
Contributor

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; }
Copy link
Contributor

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; }
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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 _));
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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?

{
Assert.Equal("{}", json.Trim());
return;
Assert.True(os.TryGetProperty(UserAgentInfoDto.OsInfo.TypeJsonKey, out _));
Copy link
Contributor

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.

Copy link
Contributor Author

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.

}

// 4. DTO serializes with expected JSON property names
// 4. DTO JSON contract - verify names and values
Copy link
Contributor

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.

Copy link
Contributor

@benrr101 benrr101 left a 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
Copy link
Contributor

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?

Copy link
Contributor Author

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!

/// <summary>
/// Maximum number of characters allowed for the driver version.
/// </summary>
private const int VersionMaxChars = 16;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please order alphabetically

Copy link
Contributor Author

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.

Copy link
Contributor

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

Copy link
Contributor

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;
Copy link
Contributor

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
Copy link
Contributor

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

Copy link
Contributor Author

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.

public const string DriverJsonKey = "driver";
public const string VersionJsonKey = "version";
public const string OsJsonKey = "os";
public const string ArchJsonKey = "arch";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort alphabetically please

public const string RuntimeJsonKey = "runtime";


// TODO: Does this need to be nullable?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to @paulmedynski - no!

#nullable enable

namespace Microsoft.Data.SqlClient.UserAgent;
internal class UserAgentInfoDto
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems unnecessary to make this a class - it could probably be done as a struct

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DTO does not satisfy criteria for a struct: https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/choosing-between-class-and-struct
1). Instance size should be under 16 bytes for a struct. DTO holds 5 strings and one 'OSInfo', around 48+bytes, triple the guideline.
2). We are mutating this object in UserAgentInfo by dropping fields, serializing and dropping again if needed(structs need to be immutable).
3). There won't be any perf benefits as a struct would introduce boxing allocations during serializations.

[JsonPropertyName(RuntimeJsonKey)]
public string? Runtime { get; set; }

public class OsInfo
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise, this could probably be a struct?

{
// 1. Cached payload is within the 2,047‑byte spec and never null
[Fact]
public void CachedPayload_IsNotNull_And_WithinSpecLimit()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can, it would be good to structure these as

// Arrange
.. any kind of setup work

// Act
.. preferably one line of action to exercise the desired code

// Assert
.. whatever checks need to be done to validate the behavior

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, agreed. I have tried to follow this structure as much as possible but grouped statements logically together. Will double check if there are any changes in accordance with the structure.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not introduce a bunch of temp variables just to capture state from Act and then Assert it a few lines below. That becomes error prone and can obscure what's happening in the tests. I would prefer to see:

Assert.Equal(foo.SomeAction(), "expected result");

than

var actualResult = foo.SomeAction();
...
Assert.Equal(actualResult, expectedResult);

If there is complicated actions and it makes sense to capture output along the way, then sure. But I would advise against introducing ways to break a test just to follow a pattern.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants