Skip to content

Commit 1b7845f

Browse files
[NSJ -> STJ]Migrate AutoComplete (#7298)
1 parent e93dc2e commit 1b7845f

9 files changed

Lines changed: 211 additions & 22 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
#if !NET9_0_OR_GREATER
5+
namespace System.Diagnostics.CodeAnalysis
6+
{
7+
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
8+
internal sealed class FeatureSwitchDefinitionAttribute : Attribute
9+
{
10+
public FeatureSwitchDefinitionAttribute(string switchName) => SwitchName = switchName;
11+
public string SwitchName { get; }
12+
}
13+
}
14+
#endif

build/Shared/NuGetFeatureFlags.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Diagnostics.CodeAnalysis;
6+
using NuGet.Common;
7+
8+
namespace NuGet.Shared
9+
{
10+
internal static class NuGetFeatureFlags
11+
{
12+
internal const string UseSystemTextJsonDeserializationSwitchName = "NuGet.UseSystemTextJsonDeserialization";
13+
internal const string UseSystemTextJsonDeserializationEnvVar = "NUGET_USE_SYSTEM_TEXT_JSON_DESERIALIZATION";
14+
15+
private static readonly Lazy<bool> _isSystemTextJsonDeserializationEnabledByEnvironment =
16+
new Lazy<bool>(() => IsSystemTextJsonDeserializationEnabledByEnvironment(EnvironmentVariableWrapper.Instance));
17+
18+
/// <summary>Feature switch for System.Text.Json deserialization. Defaults to <see langword="false"/> (Newtonsoft is the default).</summary>
19+
[FeatureSwitchDefinition(UseSystemTextJsonDeserializationSwitchName)]
20+
internal static bool UseSystemTextJsonDeserializationFeatureSwitch { get; } =
21+
AppContext.TryGetSwitch(UseSystemTextJsonDeserializationSwitchName, out bool value) && value;
22+
23+
/// <summary>Returns <see langword="true"/> when env var <c>NUGET_USE_SYSTEM_TEXT_JSON_DESERIALIZATION</c> is <c>true</c>.</summary>
24+
/// <param name="env">
25+
/// Pass <see langword="null"/> (or omit) in production code to use the cached <see cref="Lazy{T}"/> value,
26+
/// avoiding repeated allocations on .NET Framework. Pass an explicit <see cref="IEnvironmentVariableReader"/>
27+
/// only in tests to override the value.
28+
/// </param>
29+
internal static bool IsSystemTextJsonDeserializationEnabledByEnvironment(IEnvironmentVariableReader? env = null)
30+
{
31+
if (env is null)
32+
{
33+
return _isSystemTextJsonDeserializationEnabledByEnvironment.Value;
34+
}
35+
36+
string? envValue = env.GetEnvironmentVariable(UseSystemTextJsonDeserializationEnvVar);
37+
return string.Equals(envValue, "true", StringComparison.OrdinalIgnoreCase);
38+
}
39+
}
40+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace NuGet.Protocol.Model
7+
{
8+
internal sealed class AutoCompleteModel
9+
{
10+
[JsonPropertyName("data")]
11+
public string[]? Data { get; set; }
12+
}
13+
}

src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
<Compile Include="$(SharedDirectory)\HashCodeCombiner.cs" />
4040
<Compile Include="$(SharedDirectory)\NoAllocEnumerateExtensions.cs" />
4141
<Compile Include="$(SharedDirectory)\NullableAttributes.cs" />
42+
<Compile Include="$(SharedDirectory)\NuGetFeatureFlags.cs" />
43+
<Compile Include="$(SharedDirectory)\AotCompatibilityAttributes.cs" />
4244
<Compile Include="$(SharedDirectory)\SimplePool.cs" />
4345
<Compile Include="$(SharedDirectory)\StringBuilderPool.cs" />
4446
<Compile Include="$(SharedDirectory)\TaskResult.cs" />

src/NuGet.Core/NuGet.Protocol/Resources/AutoCompleteResourceV3.cs

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,38 @@
88
using System.Globalization;
99
using System.Linq;
1010
using System.Net;
11+
using System.Text.Json;
1112
using System.Threading;
1213
using System.Threading.Tasks;
1314
using Newtonsoft.Json.Linq;
15+
using NuGet.Common;
1416
using NuGet.Protocol.Core.Types;
17+
using NuGet.Protocol.Model;
18+
using NuGet.Protocol.Utility;
19+
using NuGet.Shared;
1520
using NuGet.Versioning;
1621

1722
namespace NuGet.Protocol
1823
{
1924
public class AutoCompleteResourceV3 : AutoCompleteResource
2025
{
21-
private readonly RegistrationResourceV3 _regResource;
22-
private readonly ServiceIndexResourceV3 _serviceIndex;
23-
private readonly HttpSource _client;
26+
internal readonly RegistrationResourceV3 _regResource;
27+
internal readonly ServiceIndexResourceV3 _serviceIndex;
28+
internal readonly HttpSource _client;
29+
private readonly IEnvironmentVariableReader _environmentVariableReader;
2430

2531
public AutoCompleteResourceV3(HttpSource client, ServiceIndexResourceV3 serviceIndex, RegistrationResourceV3 regResource)
32+
: this(client, serviceIndex, regResource, null)
33+
{
34+
}
35+
36+
internal AutoCompleteResourceV3(HttpSource client, ServiceIndexResourceV3 serviceIndex, RegistrationResourceV3 regResource, IEnvironmentVariableReader environmentVariableReader)
2637
: base()
2738
{
2839
_regResource = regResource;
2940
_serviceIndex = serviceIndex;
3041
_client = client;
42+
_environmentVariableReader = environmentVariableReader;
3143
}
3244

3345
public override async Task<IEnumerable<string>> IdStartsWith(
@@ -36,25 +48,54 @@ public override async Task<IEnumerable<string>> IdStartsWith(
3648
Common.ILogger log,
3749
CancellationToken token)
3850
{
39-
var searchUrl = _serviceIndex.GetServiceEntryUri(ServiceTypes.SearchAutocompleteService);
40-
41-
if (searchUrl == null)
51+
if (NuGetFeatureFlags.UseSystemTextJsonDeserializationFeatureSwitch || NuGetFeatureFlags.IsSystemTextJsonDeserializationEnabledByEnvironment(_environmentVariableReader))
4252
{
43-
throw new FatalProtocolException(Strings.Protocol_MissingSearchService);
53+
return await IdStartsWithStjAsync(packageIdPrefix, includePrerelease, log, token);
4454
}
55+
else
56+
{
57+
return await IdStartsWithNsjAsync(packageIdPrefix, includePrerelease, log, token);
58+
}
59+
}
4560

46-
// Construct the query
47-
var queryUrl = new UriBuilder(searchUrl.AbsoluteUri);
48-
var queryString =
49-
"q=" + WebUtility.UrlEncode(packageIdPrefix) +
50-
"&prerelease=" + includePrerelease.ToString(CultureInfo.CurrentCulture).ToLowerInvariant() +
51-
"&semVerLevel=2.0.0";
61+
private async Task<IEnumerable<string>> IdStartsWithStjAsync(
62+
string packageIdPrefix,
63+
bool includePrerelease,
64+
Common.ILogger log,
65+
CancellationToken token)
66+
{
67+
var queryUri = BuildQueryUri(packageIdPrefix, includePrerelease);
68+
Common.ILogger logger = log ?? Common.NullLogger.Instance;
69+
70+
AutoCompleteModel results = await _client.ProcessStreamAsync(
71+
new HttpSourceRequest(queryUri, logger),
72+
async stream =>
73+
{
74+
if (stream == null)
75+
{
76+
return null;
77+
}
5278

53-
queryUrl.Query = queryString;
79+
return await JsonSerializer.DeserializeAsync(stream, JsonContext.Default.AutoCompleteModel, token);
80+
},
81+
logger,
82+
token);
5483

84+
token.ThrowIfCancellationRequested();
85+
86+
return results?.Data?.Where(item => item != null && item.StartsWith(packageIdPrefix, StringComparison.OrdinalIgnoreCase))
87+
?? [];
88+
}
89+
90+
private async Task<IEnumerable<string>> IdStartsWithNsjAsync(
91+
string packageIdPrefix,
92+
bool includePrerelease,
93+
Common.ILogger log,
94+
CancellationToken token)
95+
{
96+
var queryUri = BuildQueryUri(packageIdPrefix, includePrerelease);
5597
Common.ILogger logger = log ?? Common.NullLogger.Instance;
5698

57-
var queryUri = queryUrl.Uri;
5899
var results = await _client.GetJObjectAsync(
59100
new HttpSourceRequest(queryUri, logger),
60101
logger,
@@ -83,6 +124,25 @@ public override async Task<IEnumerable<string>> IdStartsWith(
83124
return outputs.Where(item => item.StartsWith(packageIdPrefix, StringComparison.OrdinalIgnoreCase));
84125
}
85126

127+
private Uri BuildQueryUri(string packageIdPrefix, bool includePrerelease)
128+
{
129+
var searchUrl = _serviceIndex.GetServiceEntryUri(ServiceTypes.SearchAutocompleteService);
130+
131+
if (searchUrl == null)
132+
{
133+
throw new FatalProtocolException(Strings.Protocol_MissingSearchService);
134+
}
135+
136+
// Construct the query
137+
var queryUrl = new UriBuilder(searchUrl.AbsoluteUri);
138+
queryUrl.Query =
139+
"q=" + WebUtility.UrlEncode(packageIdPrefix) +
140+
"&prerelease=" + includePrerelease.ToString(CultureInfo.CurrentCulture).ToLowerInvariant() +
141+
"&semVerLevel=2.0.0";
142+
143+
return queryUrl.Uri;
144+
}
145+
86146
public override async Task<IEnumerable<NuGetVersion>> VersionStartsWith(
87147
string packageId,
88148
string versionPrefix,

src/NuGet.Core/NuGet.Protocol/Utility/JsonContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace NuGet.Protocol.Utility
1717
[JsonSerializable(typeof(HttpFileSystemBasedFindPackageByIdResource.FlatContainerVersionList))]
1818
[JsonSerializable(typeof(IReadOnlyList<V3VulnerabilityIndexEntry>), TypeInfoPropertyName = "VulnerabilityIndex")]
1919
[JsonSerializable(typeof(CaseInsensitiveDictionary<IReadOnlyList<PackageVulnerabilityInfo>>), TypeInfoPropertyName = "VulnerabilityPage")]
20+
[JsonSerializable(typeof(AutoCompleteModel))]
2021
internal partial class JsonContext : JsonSerializerContext
2122
{
2223
}

test/NuGet.Core.Tests/NuGet.Protocol.Tests/AutoCompleteResourceV3Tests.cs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
using System.Linq;
66
using System.Threading;
77
using System.Threading.Tasks;
8+
using Moq;
9+
using NuGet.Common;
810
using NuGet.Protocol.Core.Types;
11+
using NuGet.Shared;
912
using NuGet.Test.Utility;
1013
using Test.Utility;
1114
using Xunit;
@@ -14,23 +17,27 @@ namespace NuGet.Protocol.Tests
1417
{
1518
public class AutoCompleteResourceV3Tests
1619
{
17-
[Fact]
18-
public async Task AutoCompleteResourceV3_IdStartsWithAsync()
20+
[Theory]
21+
[InlineData("true")] // STJ path
22+
[InlineData("false")] // NSJ path
23+
public async Task IdStartsWith_BothPaths_ReturnsResultsAsync(string useStj)
1924
{
2025
// Arrange
2126
var responses = new Dictionary<string, string>();
22-
const string sourceName = "http://testsource.com/v3/index.json";
23-
responses.Add(sourceName, JsonData.IndexWithoutFlatContainer);
27+
responses.Add("http://testsource.test/v3/index.json", JsonData.IndexWithoutFlatContainer);
2428
responses.Add("https://api-v3search-0.nuget.org/autocomplete?q=newt&prerelease=true&semVerLevel=2.0.0",
2529
JsonData.AutoCompleteEndpointNewtResult);
2630

27-
var repo = StaticHttpHandler.CreateSource(sourceName, Repository.Provider.GetCoreV3(), responses);
28-
var resource = await repo.GetResourceAsync<AutoCompleteResource>(CancellationToken.None);
31+
var repo = StaticHttpHandler.CreateSource("http://testsource.test/v3/index.json", Repository.Provider.GetCoreV3(), responses);
32+
var resource = (AutoCompleteResourceV3)await repo.GetResourceAsync<AutoCompleteResource>(CancellationToken.None);
2933

34+
var envReader = new Mock<IEnvironmentVariableReader>();
35+
envReader.Setup(e => e.GetEnvironmentVariable(NuGetFeatureFlags.UseSystemTextJsonDeserializationEnvVar)).Returns(useStj);
36+
var testResource = new AutoCompleteResourceV3(resource._client, resource._serviceIndex, resource._regResource, envReader.Object);
3037
var logger = new TestLogger();
3138

3239
// Act
33-
var result = await resource.IdStartsWith("newt", true, logger, CancellationToken.None);
40+
var result = await testResource.IdStartsWith("newt", true, logger, CancellationToken.None);
3441

3542
// Assert
3643
Assert.Equal(10, result.Count());
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using NuGet.Shared;
6+
using Test.Utility;
7+
using Xunit;
8+
9+
namespace NuGet.Protocol.Tests
10+
{
11+
public class NuGetFeatureFlagsTests
12+
{
13+
[Fact]
14+
public void UseSystemTextJsonDeserializationFeatureSwitch_Default_ReturnsFalse()
15+
{
16+
Assert.False(NuGetFeatureFlags.UseSystemTextJsonDeserializationFeatureSwitch);
17+
}
18+
19+
[Fact]
20+
public void IsSystemTextJsonDeserializationEnabledByEnvironment_WhenEnvVarNotSet_ReturnsFalse()
21+
{
22+
Assert.False(NuGetFeatureFlags.IsSystemTextJsonDeserializationEnabledByEnvironment(TestEnvironmentVariableReader.EmptyInstance));
23+
}
24+
25+
[Theory]
26+
[InlineData("true")]
27+
[InlineData("True")]
28+
[InlineData("TRUE")]
29+
public void IsSystemTextJsonDeserializationEnabledByEnvironment_WhenEnvVarSetToTrue_ReturnsTrue(string value)
30+
{
31+
var env = new TestEnvironmentVariableReader(
32+
new Dictionary<string, string> { [NuGetFeatureFlags.UseSystemTextJsonDeserializationEnvVar] = value });
33+
34+
Assert.True(NuGetFeatureFlags.IsSystemTextJsonDeserializationEnabledByEnvironment(env));
35+
}
36+
37+
[Theory]
38+
[InlineData("false")]
39+
[InlineData("0")]
40+
[InlineData("1")]
41+
[InlineData("anything")]
42+
public void IsSystemTextJsonDeserializationEnabledByEnvironment_WhenEnvVarSetToFalseOrUnrecognized_ReturnsFalse(string value)
43+
{
44+
var env = new TestEnvironmentVariableReader(
45+
new Dictionary<string, string> { [NuGetFeatureFlags.UseSystemTextJsonDeserializationEnvVar] = value });
46+
47+
Assert.False(NuGetFeatureFlags.IsSystemTextJsonDeserializationEnabledByEnvironment(env));
48+
}
49+
}
50+
}

test/NuGet.Core.Tests/NuGet.Shared.Tests/NuGet.Shared.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
<Compile Include="$(SharedDirectory)\*.cs" Exclude="bin\**;obj\**;**\*.xproj;packages\**" />
1010
<Compile Remove="$(SharedDirectory)\IsExternalInit.cs" />
1111
<Compile Remove="$(SharedDirectory)\RequiredModifierAttributes.cs" />
12+
<Compile Remove="$(SharedDirectory)\AotCompatibilityAttributes.cs" />
13+
<Compile Remove="$(SharedDirectory)\NuGetFeatureFlags.cs" />
1214
</ItemGroup>
1315
<ItemGroup>
1416
<ProjectReference Include="..\..\TestUtilities\Test.Utility\Test.Utility.csproj" />

0 commit comments

Comments
 (0)