From 26cdcd1b9271dfec48112a48ad6c1fb6b0dcb972 Mon Sep 17 00:00:00 2001 From: Nigusu Yenework Date: Fri, 17 Apr 2026 11:33:20 -0700 Subject: [PATCH 1/5] Migrate AutoComplete --- .../NuGet.Protocol/Model/AutoCompleteModel.cs | 13 +++++++ .../Resources/AutoCompleteResourceV3.cs | 37 +++++++++---------- .../NuGet.Protocol/Utility/JsonContext.cs | 5 +++ 3 files changed, 36 insertions(+), 19 deletions(-) create mode 100644 src/NuGet.Core/NuGet.Protocol/Model/AutoCompleteModel.cs diff --git a/src/NuGet.Core/NuGet.Protocol/Model/AutoCompleteModel.cs b/src/NuGet.Core/NuGet.Protocol/Model/AutoCompleteModel.cs new file mode 100644 index 00000000000..1a02756fd90 --- /dev/null +++ b/src/NuGet.Core/NuGet.Protocol/Model/AutoCompleteModel.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Text.Json.Serialization; + +namespace NuGet.Protocol.Model +{ + internal sealed class AutoCompleteModel + { + [JsonPropertyName("data")] + public string[]? Data { get; set; } + } +} diff --git a/src/NuGet.Core/NuGet.Protocol/Resources/AutoCompleteResourceV3.cs b/src/NuGet.Core/NuGet.Protocol/Resources/AutoCompleteResourceV3.cs index 9a34a284623..4dc3003c30e 100644 --- a/src/NuGet.Core/NuGet.Protocol/Resources/AutoCompleteResourceV3.cs +++ b/src/NuGet.Core/NuGet.Protocol/Resources/AutoCompleteResourceV3.cs @@ -8,10 +8,12 @@ using System.Globalization; using System.Linq; using System.Net; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Newtonsoft.Json.Linq; using NuGet.Protocol.Core.Types; +using NuGet.Protocol.Model; +using NuGet.Protocol.Utility; using NuGet.Versioning; namespace NuGet.Protocol @@ -55,32 +57,29 @@ public override async Task> IdStartsWith( Common.ILogger logger = log ?? Common.NullLogger.Instance; var queryUri = queryUrl.Uri; - var results = await _client.GetJObjectAsync( + AutoCompleteModel results = await _client.ProcessStreamAsync( new HttpSourceRequest(queryUri, logger), + async stream => + { + if (stream == null) + { + return null; + } + + return await JsonSerializer.DeserializeAsync(stream, JsonContext.Default.AutoCompleteModel, token); + }, logger, token); + token.ThrowIfCancellationRequested(); - if (results == null) - { - return Enumerable.Empty(); - } - var data = results.Value("data"); - if (data == null) - { - return Enumerable.Empty(); - } - // Resolve all the objects - var outputs = new List(); - foreach (var result in data) + if (results?.Data == null) { - if (result != null) - { - outputs.Add(result.ToString()); - } + return Enumerable.Empty(); } - return outputs.Where(item => item.StartsWith(packageIdPrefix, StringComparison.OrdinalIgnoreCase)); + return results.Data + .Where(item => item != null && item.StartsWith(packageIdPrefix, StringComparison.OrdinalIgnoreCase)); } public override async Task> VersionStartsWith( diff --git a/src/NuGet.Core/NuGet.Protocol/Utility/JsonContext.cs b/src/NuGet.Core/NuGet.Protocol/Utility/JsonContext.cs index 2a181357a86..24f8f259bf9 100644 --- a/src/NuGet.Core/NuGet.Protocol/Utility/JsonContext.cs +++ b/src/NuGet.Core/NuGet.Protocol/Utility/JsonContext.cs @@ -17,6 +17,11 @@ namespace NuGet.Protocol.Utility [JsonSerializable(typeof(HttpFileSystemBasedFindPackageByIdResource.FlatContainerVersionList))] [JsonSerializable(typeof(IReadOnlyList), TypeInfoPropertyName = "VulnerabilityIndex")] [JsonSerializable(typeof(CaseInsensitiveDictionary>), TypeInfoPropertyName = "VulnerabilityPage")] +<<<<<<< Updated upstream +======= + [JsonSerializable(typeof(AutoCompleteModel))] + [JsonSerializable(typeof(ServiceIndexModel))] +>>>>>>> Stashed changes internal partial class JsonContext : JsonSerializerContext { } From 6f4e7972bd481c78b98b19c9e61883fb37ec29c2 Mon Sep 17 00:00:00 2001 From: Nigusu Yenework Date: Fri, 17 Apr 2026 11:35:15 -0700 Subject: [PATCH 2/5] Fix --- src/NuGet.Core/NuGet.Protocol/Utility/JsonContext.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/NuGet.Core/NuGet.Protocol/Utility/JsonContext.cs b/src/NuGet.Core/NuGet.Protocol/Utility/JsonContext.cs index 24f8f259bf9..a8ad9d7462b 100644 --- a/src/NuGet.Core/NuGet.Protocol/Utility/JsonContext.cs +++ b/src/NuGet.Core/NuGet.Protocol/Utility/JsonContext.cs @@ -17,11 +17,7 @@ namespace NuGet.Protocol.Utility [JsonSerializable(typeof(HttpFileSystemBasedFindPackageByIdResource.FlatContainerVersionList))] [JsonSerializable(typeof(IReadOnlyList), TypeInfoPropertyName = "VulnerabilityIndex")] [JsonSerializable(typeof(CaseInsensitiveDictionary>), TypeInfoPropertyName = "VulnerabilityPage")] -<<<<<<< Updated upstream -======= [JsonSerializable(typeof(AutoCompleteModel))] - [JsonSerializable(typeof(ServiceIndexModel))] ->>>>>>> Stashed changes internal partial class JsonContext : JsonSerializerContext { } From e3947637d7de4ffdda9d460c73c87d931431689e Mon Sep 17 00:00:00 2001 From: Nigusu Yenework Date: Tue, 21 Apr 2026 14:11:17 -0700 Subject: [PATCH 3/5] Add swithces for enabling NSJ deserialization --- build/Shared/AotCompatibilityAttributes.cs | 14 +++++ build/Shared/NuGetFeatureFlags.cs | 35 +++++++++++++ .../NuGet.Protocol/NuGet.Protocol.csproj | 2 + .../NuGetFeatureFlagsTests.cs | 51 +++++++++++++++++++ .../NuGet.Shared.Tests.csproj | 2 + 5 files changed, 104 insertions(+) create mode 100644 build/Shared/AotCompatibilityAttributes.cs create mode 100644 build/Shared/NuGetFeatureFlags.cs create mode 100644 test/NuGet.Core.Tests/NuGet.Protocol.Tests/NuGetFeatureFlagsTests.cs diff --git a/build/Shared/AotCompatibilityAttributes.cs b/build/Shared/AotCompatibilityAttributes.cs new file mode 100644 index 00000000000..4423a971a0c --- /dev/null +++ b/build/Shared/AotCompatibilityAttributes.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if !NET9_0_OR_GREATER +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.Property, Inherited = false)] + internal sealed class FeatureSwitchDefinitionAttribute : Attribute + { + public FeatureSwitchDefinitionAttribute(string switchName) => SwitchName = switchName; + public string SwitchName { get; } + } +} +#endif diff --git a/build/Shared/NuGetFeatureFlags.cs b/build/Shared/NuGetFeatureFlags.cs new file mode 100644 index 00000000000..eb411bec058 --- /dev/null +++ b/build/Shared/NuGetFeatureFlags.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using NuGet.Common; + +namespace NuGet.Shared +{ + internal static class NuGetFeatureFlags + { + internal const string UsesNSJDeserializationSwitchName = "NuGet.UsesNSJDeserialization"; + internal const string UsesNSJDeserializationEnvVar = "NUGET_USES_NSJ_DESERIALIZATION"; + + private static readonly Lazy _isNSJDeserializationEnabledByEnvironment = + new Lazy(() => IsNSJDeserializationEnabledByEnvironment(EnvironmentVariableWrapper.Instance)); + + /// Feature switch for NSJ deserialization. Defaults to . + [FeatureSwitchDefinition(UsesNSJDeserializationSwitchName)] + internal static bool NSJDeserializationFeatureSwitch { get; } = + !AppContext.TryGetSwitch(UsesNSJDeserializationSwitchName, out bool value) || value; + + /// Returns when env var NUGET_USES_NSJ_DESERIALIZATION is false. + internal static bool IsNSJDeserializationEnabledByEnvironment(IEnvironmentVariableReader? env = null) + { + if (env is null) + { + return _isNSJDeserializationEnabledByEnvironment.Value; + } + + string? envValue = env.GetEnvironmentVariable(UsesNSJDeserializationEnvVar); + return !string.Equals(envValue, "false", StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj b/src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj index 5ed856305a9..4e88888de0f 100644 --- a/src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj +++ b/src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj @@ -39,6 +39,8 @@ + + diff --git a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/NuGetFeatureFlagsTests.cs b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/NuGetFeatureFlagsTests.cs new file mode 100644 index 00000000000..6594340b36b --- /dev/null +++ b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/NuGetFeatureFlagsTests.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using NuGet.Shared; +using Test.Utility; +using Xunit; + +namespace NuGet.Protocol.Tests +{ + public class NuGetFeatureFlagsTests + { + [Fact] + public void NSJDeserializationFeatureSwitch_Default_ReturnsTrue() + { + Assert.True(NuGetFeatureFlags.NSJDeserializationFeatureSwitch); + } + + [Fact] + public void IsNSJDeserializationEnabledByEnvironment_WhenEnvVarNotSet_ReturnsTrue() + { + Assert.True(NuGetFeatureFlags.IsNSJDeserializationEnabledByEnvironment(TestEnvironmentVariableReader.EmptyInstance)); + } + + [Theory] + [InlineData("false")] + [InlineData("False")] + [InlineData("FALSE")] + public void IsNSJDeserializationEnabledByEnvironment_WhenEnvVarSetToFalse_ReturnsFalse(string value) + { + var env = new TestEnvironmentVariableReader( + new Dictionary { [NuGetFeatureFlags.UsesNSJDeserializationEnvVar] = value }); + + Assert.False(NuGetFeatureFlags.IsNSJDeserializationEnabledByEnvironment(env)); + } + + [Theory] + [InlineData("true")] + [InlineData("True")] + [InlineData("0")] + [InlineData("1")] + [InlineData("anything")] + public void IsNSJDeserializationEnabledByEnvironment_WhenEnvVarSetToTrueOrUnrecognized_ReturnsTrue(string value) + { + var env = new TestEnvironmentVariableReader( + new Dictionary { [NuGetFeatureFlags.UsesNSJDeserializationEnvVar] = value }); + + Assert.True(NuGetFeatureFlags.IsNSJDeserializationEnabledByEnvironment(env)); + } + } +} diff --git a/test/NuGet.Core.Tests/NuGet.Shared.Tests/NuGet.Shared.Tests.csproj b/test/NuGet.Core.Tests/NuGet.Shared.Tests/NuGet.Shared.Tests.csproj index ec64224afc9..9450cdf59cb 100644 --- a/test/NuGet.Core.Tests/NuGet.Shared.Tests/NuGet.Shared.Tests.csproj +++ b/test/NuGet.Core.Tests/NuGet.Shared.Tests/NuGet.Shared.Tests.csproj @@ -9,6 +9,8 @@ + + From b8de2a62ba013e182c0f075ae8289a3e2de9c45d Mon Sep 17 00:00:00 2001 From: Nigusu Yenework Date: Wed, 22 Apr 2026 17:12:13 -0700 Subject: [PATCH 4/5] cleanup --- build/Shared/NuGetFeatureFlags.cs | 18 ++--- .../Resources/AutoCompleteResourceV3.cs | 65 +++++++++++++++++-- .../AutoCompleteResourceV3Tests.cs | 26 ++++---- .../NuGetFeatureFlagsTests.cs | 29 ++++----- 4 files changed, 96 insertions(+), 42 deletions(-) diff --git a/build/Shared/NuGetFeatureFlags.cs b/build/Shared/NuGetFeatureFlags.cs index eb411bec058..4567a385493 100644 --- a/build/Shared/NuGetFeatureFlags.cs +++ b/build/Shared/NuGetFeatureFlags.cs @@ -9,18 +9,18 @@ namespace NuGet.Shared { internal static class NuGetFeatureFlags { - internal const string UsesNSJDeserializationSwitchName = "NuGet.UsesNSJDeserialization"; - internal const string UsesNSJDeserializationEnvVar = "NUGET_USES_NSJ_DESERIALIZATION"; + internal const string UseNSJDeserializationSwitchName = "NuGet.UseNSJDeserialization"; + internal const string UseNSJDeserializationEnvVar = "NUGET_USE_NSJ_DESERIALIZATION"; private static readonly Lazy _isNSJDeserializationEnabledByEnvironment = new Lazy(() => IsNSJDeserializationEnabledByEnvironment(EnvironmentVariableWrapper.Instance)); - /// Feature switch for NSJ deserialization. Defaults to . - [FeatureSwitchDefinition(UsesNSJDeserializationSwitchName)] - internal static bool NSJDeserializationFeatureSwitch { get; } = - !AppContext.TryGetSwitch(UsesNSJDeserializationSwitchName, out bool value) || value; + /// Feature switch for NSJ deserialization. Defaults to (STJ is the default). + [FeatureSwitchDefinition(UseNSJDeserializationSwitchName)] + internal static bool UseNSJDeserializationFeatureSwitch { get; } = + AppContext.TryGetSwitch(UseNSJDeserializationSwitchName, out bool value) && value; - /// Returns when env var NUGET_USES_NSJ_DESERIALIZATION is false. + /// Returns when env var NUGET_USE_NSJ_DESERIALIZATION is true. internal static bool IsNSJDeserializationEnabledByEnvironment(IEnvironmentVariableReader? env = null) { if (env is null) @@ -28,8 +28,8 @@ internal static bool IsNSJDeserializationEnabledByEnvironment(IEnvironmentVariab return _isNSJDeserializationEnabledByEnvironment.Value; } - string? envValue = env.GetEnvironmentVariable(UsesNSJDeserializationEnvVar); - return !string.Equals(envValue, "false", StringComparison.OrdinalIgnoreCase); + string? envValue = env.GetEnvironmentVariable(UseNSJDeserializationEnvVar); + return string.Equals(envValue, "true", StringComparison.OrdinalIgnoreCase); } } } diff --git a/src/NuGet.Core/NuGet.Protocol/Resources/AutoCompleteResourceV3.cs b/src/NuGet.Core/NuGet.Protocol/Resources/AutoCompleteResourceV3.cs index 4dc3003c30e..68d62181e0e 100644 --- a/src/NuGet.Core/NuGet.Protocol/Resources/AutoCompleteResourceV3.cs +++ b/src/NuGet.Core/NuGet.Protocol/Resources/AutoCompleteResourceV3.cs @@ -11,18 +11,22 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using NuGet.Common; using NuGet.Protocol.Core.Types; using NuGet.Protocol.Model; using NuGet.Protocol.Utility; +using NuGet.Shared; using NuGet.Versioning; namespace NuGet.Protocol { public class AutoCompleteResourceV3 : AutoCompleteResource { - private readonly RegistrationResourceV3 _regResource; - private readonly ServiceIndexResourceV3 _serviceIndex; - private readonly HttpSource _client; + internal readonly RegistrationResourceV3 _regResource; + internal readonly ServiceIndexResourceV3 _serviceIndex; + internal readonly HttpSource _client; + private readonly IEnvironmentVariableReader _environmentVariableReader; public AutoCompleteResourceV3(HttpSource client, ServiceIndexResourceV3 serviceIndex, RegistrationResourceV3 regResource) : base() @@ -30,6 +34,16 @@ public AutoCompleteResourceV3(HttpSource client, ServiceIndexResourceV3 serviceI _regResource = regResource; _serviceIndex = serviceIndex; _client = client; + _environmentVariableReader = EnvironmentVariableWrapper.Instance; + } + + internal AutoCompleteResourceV3(HttpSource client, ServiceIndexResourceV3 serviceIndex, RegistrationResourceV3 regResource, IEnvironmentVariableReader environmentVariableReader) + : base() + { + _regResource = regResource; + _serviceIndex = serviceIndex; + _client = client; + _environmentVariableReader = environmentVariableReader; } public override async Task> IdStartsWith( @@ -55,8 +69,13 @@ public override async Task> IdStartsWith( queryUrl.Query = queryString; Common.ILogger logger = log ?? Common.NullLogger.Instance; - var queryUri = queryUrl.Uri; + + if (NuGetFeatureFlags.UseNSJDeserializationFeatureSwitch || NuGetFeatureFlags.IsNSJDeserializationEnabledByEnvironment(_environmentVariableReader)) + { + return await IdStartsWithNsjAsync(packageIdPrefix, logger, queryUri, token); + } + AutoCompleteModel results = await _client.ProcessStreamAsync( new HttpSourceRequest(queryUri, logger), async stream => @@ -73,13 +92,45 @@ public override async Task> IdStartsWith( token.ThrowIfCancellationRequested(); - if (results?.Data == null) + return results?.Data?.Where(item => item != null && item.StartsWith(packageIdPrefix, StringComparison.OrdinalIgnoreCase)) + ?? []; + } + + private async Task> IdStartsWithNsjAsync( + string packageIdPrefix, + Common.ILogger logger, + Uri queryUri, + CancellationToken token) + { + var results = await _client.GetJObjectAsync( + new HttpSourceRequest(queryUri, logger), + logger, + token); + + token.ThrowIfCancellationRequested(); + + if (results == null) + { + return Enumerable.Empty(); + } + + var data = results.Value("data"); + if (data == null) { return Enumerable.Empty(); } - return results.Data - .Where(item => item != null && item.StartsWith(packageIdPrefix, StringComparison.OrdinalIgnoreCase)); + // Resolve all the objects + var outputs = new List(); + foreach (var result in data) + { + if (result != null) + { + outputs.Add(result.ToString()); + } + } + + return outputs.Where(item => item.StartsWith(packageIdPrefix, StringComparison.OrdinalIgnoreCase)); } public override async Task> VersionStartsWith( diff --git a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/AutoCompleteResourceV3Tests.cs b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/AutoCompleteResourceV3Tests.cs index ca3fd189b31..0a7a6c716ca 100644 --- a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/AutoCompleteResourceV3Tests.cs +++ b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/AutoCompleteResourceV3Tests.cs @@ -5,7 +5,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Moq; +using NuGet.Common; using NuGet.Protocol.Core.Types; +using NuGet.Shared; using NuGet.Test.Utility; using Test.Utility; using Xunit; @@ -14,25 +17,26 @@ namespace NuGet.Protocol.Tests { public class AutoCompleteResourceV3Tests { - [Fact] - public async Task AutoCompleteResourceV3_IdStartsWithAsync() + [Theory] + [InlineData("true")] // NSJ path + [InlineData("false")] // STJ path + public async Task IdStartsWith_BothPaths_ReturnsResultsAsync(string useNsj) { - // Arrange var responses = new Dictionary(); - const string sourceName = "http://testsource.com/v3/index.json"; - responses.Add(sourceName, JsonData.IndexWithoutFlatContainer); + responses.Add("http://testsource.com/v3/index.json", JsonData.IndexWithoutFlatContainer); responses.Add("https://api-v3search-0.nuget.org/autocomplete?q=newt&prerelease=true&semVerLevel=2.0.0", JsonData.AutoCompleteEndpointNewtResult); - var repo = StaticHttpHandler.CreateSource(sourceName, Repository.Provider.GetCoreV3(), responses); - var resource = await repo.GetResourceAsync(CancellationToken.None); + var repo = StaticHttpHandler.CreateSource("http://testsource.com/v3/index.json", Repository.Provider.GetCoreV3(), responses); + var resource = (AutoCompleteResourceV3)await repo.GetResourceAsync(CancellationToken.None); - var logger = new TestLogger(); + var envReader = new Mock(); + envReader.Setup(e => e.GetEnvironmentVariable(NuGetFeatureFlags.UseNSJDeserializationEnvVar)).Returns(useNsj); + var testResource = new AutoCompleteResourceV3(resource._client, resource._serviceIndex, resource._regResource, envReader.Object); - // Act - var result = await resource.IdStartsWith("newt", true, logger, CancellationToken.None); + var logger = new TestLogger(); + var result = await testResource.IdStartsWith("newt", true, logger, CancellationToken.None); - // Assert Assert.Equal(10, result.Count()); Assert.NotEmpty(logger.Messages); } diff --git a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/NuGetFeatureFlagsTests.cs b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/NuGetFeatureFlagsTests.cs index 6594340b36b..68e6bd7797c 100644 --- a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/NuGetFeatureFlagsTests.cs +++ b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/NuGetFeatureFlagsTests.cs @@ -11,41 +11,40 @@ namespace NuGet.Protocol.Tests public class NuGetFeatureFlagsTests { [Fact] - public void NSJDeserializationFeatureSwitch_Default_ReturnsTrue() + public void UseNSJDeserializationFeatureSwitch_Default_ReturnsFalse() { - Assert.True(NuGetFeatureFlags.NSJDeserializationFeatureSwitch); + Assert.False(NuGetFeatureFlags.UseNSJDeserializationFeatureSwitch); } [Fact] - public void IsNSJDeserializationEnabledByEnvironment_WhenEnvVarNotSet_ReturnsTrue() + public void IsNSJDeserializationEnabledByEnvironment_WhenEnvVarNotSet_ReturnsFalse() { - Assert.True(NuGetFeatureFlags.IsNSJDeserializationEnabledByEnvironment(TestEnvironmentVariableReader.EmptyInstance)); + Assert.False(NuGetFeatureFlags.IsNSJDeserializationEnabledByEnvironment(TestEnvironmentVariableReader.EmptyInstance)); } [Theory] - [InlineData("false")] - [InlineData("False")] - [InlineData("FALSE")] - public void IsNSJDeserializationEnabledByEnvironment_WhenEnvVarSetToFalse_ReturnsFalse(string value) + [InlineData("true")] + [InlineData("True")] + [InlineData("TRUE")] + public void IsNSJDeserializationEnabledByEnvironment_WhenEnvVarSetToTrue_ReturnsTrue(string value) { var env = new TestEnvironmentVariableReader( - new Dictionary { [NuGetFeatureFlags.UsesNSJDeserializationEnvVar] = value }); + new Dictionary { [NuGetFeatureFlags.UseNSJDeserializationEnvVar] = value }); - Assert.False(NuGetFeatureFlags.IsNSJDeserializationEnabledByEnvironment(env)); + Assert.True(NuGetFeatureFlags.IsNSJDeserializationEnabledByEnvironment(env)); } [Theory] - [InlineData("true")] - [InlineData("True")] + [InlineData("false")] [InlineData("0")] [InlineData("1")] [InlineData("anything")] - public void IsNSJDeserializationEnabledByEnvironment_WhenEnvVarSetToTrueOrUnrecognized_ReturnsTrue(string value) + public void IsNSJDeserializationEnabledByEnvironment_WhenEnvVarSetToFalseOrUnrecognized_ReturnsFalse(string value) { var env = new TestEnvironmentVariableReader( - new Dictionary { [NuGetFeatureFlags.UsesNSJDeserializationEnvVar] = value }); + new Dictionary { [NuGetFeatureFlags.UseNSJDeserializationEnvVar] = value }); - Assert.True(NuGetFeatureFlags.IsNSJDeserializationEnabledByEnvironment(env)); + Assert.False(NuGetFeatureFlags.IsNSJDeserializationEnabledByEnvironment(env)); } } } From 59f0d3b7801e1d12545a889cb93e6418854542bd Mon Sep 17 00:00:00 2001 From: Nigusu Yenework Date: Thu, 23 Apr 2026 03:36:55 -0700 Subject: [PATCH 5/5] comments --- build/Shared/NuGetFeatureFlags.cs | 29 +++++---- .../Resources/AutoCompleteResourceV3.cs | 63 +++++++++++-------- .../AutoCompleteResourceV3Tests.cs | 11 ++-- .../NuGetFeatureFlagsTests.cs | 20 +++--- 4 files changed, 72 insertions(+), 51 deletions(-) diff --git a/build/Shared/NuGetFeatureFlags.cs b/build/Shared/NuGetFeatureFlags.cs index 4567a385493..f51979b9913 100644 --- a/build/Shared/NuGetFeatureFlags.cs +++ b/build/Shared/NuGetFeatureFlags.cs @@ -9,26 +9,31 @@ namespace NuGet.Shared { internal static class NuGetFeatureFlags { - internal const string UseNSJDeserializationSwitchName = "NuGet.UseNSJDeserialization"; - internal const string UseNSJDeserializationEnvVar = "NUGET_USE_NSJ_DESERIALIZATION"; + internal const string UseLegacyJsonDeserializationSwitchName = "NuGet.UseLegacyJsonDeserialization"; + internal const string UseLegacyJsonDeserializationEnvVar = "NUGET_USE_LEGACY_JSON_DESERIALIZATION"; - private static readonly Lazy _isNSJDeserializationEnabledByEnvironment = - new Lazy(() => IsNSJDeserializationEnabledByEnvironment(EnvironmentVariableWrapper.Instance)); + private static readonly Lazy _isLegacyJsonDeserializationEnabledByEnvironment = + new Lazy(() => IsLegacyJsonDeserializationEnabledByEnvironment(EnvironmentVariableWrapper.Instance)); - /// Feature switch for NSJ deserialization. Defaults to (STJ is the default). - [FeatureSwitchDefinition(UseNSJDeserializationSwitchName)] - internal static bool UseNSJDeserializationFeatureSwitch { get; } = - AppContext.TryGetSwitch(UseNSJDeserializationSwitchName, out bool value) && value; + /// Feature switch for legacy (Newtonsoft) JSON deserialization. Defaults to (STJ is the default). + [FeatureSwitchDefinition(UseLegacyJsonDeserializationSwitchName)] + internal static bool UseLegacyJsonDeserializationFeatureSwitch { get; } = + AppContext.TryGetSwitch(UseLegacyJsonDeserializationSwitchName, out bool value) && value; - /// Returns when env var NUGET_USE_NSJ_DESERIALIZATION is true. - internal static bool IsNSJDeserializationEnabledByEnvironment(IEnvironmentVariableReader? env = null) + /// Returns when env var NUGET_USE_LEGACY_JSON_DESERIALIZATION is true. + /// + /// Pass (or omit) in production code to use the cached value, + /// avoiding repeated allocations on .NET Framework. Pass an explicit + /// only in tests to override the value. + /// + internal static bool IsLegacyJsonDeserializationEnabledByEnvironment(IEnvironmentVariableReader? env = null) { if (env is null) { - return _isNSJDeserializationEnabledByEnvironment.Value; + return _isLegacyJsonDeserializationEnabledByEnvironment.Value; } - string? envValue = env.GetEnvironmentVariable(UseNSJDeserializationEnvVar); + string? envValue = env.GetEnvironmentVariable(UseLegacyJsonDeserializationEnvVar); return string.Equals(envValue, "true", StringComparison.OrdinalIgnoreCase); } } diff --git a/src/NuGet.Core/NuGet.Protocol/Resources/AutoCompleteResourceV3.cs b/src/NuGet.Core/NuGet.Protocol/Resources/AutoCompleteResourceV3.cs index 68d62181e0e..51b9068f3d5 100644 --- a/src/NuGet.Core/NuGet.Protocol/Resources/AutoCompleteResourceV3.cs +++ b/src/NuGet.Core/NuGet.Protocol/Resources/AutoCompleteResourceV3.cs @@ -34,7 +34,6 @@ public AutoCompleteResourceV3(HttpSource client, ServiceIndexResourceV3 serviceI _regResource = regResource; _serviceIndex = serviceIndex; _client = client; - _environmentVariableReader = EnvironmentVariableWrapper.Instance; } internal AutoCompleteResourceV3(HttpSource client, ServiceIndexResourceV3 serviceIndex, RegistrationResourceV3 regResource, IEnvironmentVariableReader environmentVariableReader) @@ -52,29 +51,24 @@ public override async Task> IdStartsWith( Common.ILogger log, CancellationToken token) { - var searchUrl = _serviceIndex.GetServiceEntryUri(ServiceTypes.SearchAutocompleteService); - - if (searchUrl == null) + if (NuGetFeatureFlags.UseLegacyJsonDeserializationFeatureSwitch || NuGetFeatureFlags.IsLegacyJsonDeserializationEnabledByEnvironment(_environmentVariableReader)) { - throw new FatalProtocolException(Strings.Protocol_MissingSearchService); + return await IdStartsWithNsjAsync(packageIdPrefix, includePrerelease, log, token); } - - // Construct the query - var queryUrl = new UriBuilder(searchUrl.AbsoluteUri); - var queryString = - "q=" + WebUtility.UrlEncode(packageIdPrefix) + - "&prerelease=" + includePrerelease.ToString(CultureInfo.CurrentCulture).ToLowerInvariant() + - "&semVerLevel=2.0.0"; - - queryUrl.Query = queryString; - - Common.ILogger logger = log ?? Common.NullLogger.Instance; - var queryUri = queryUrl.Uri; - - if (NuGetFeatureFlags.UseNSJDeserializationFeatureSwitch || NuGetFeatureFlags.IsNSJDeserializationEnabledByEnvironment(_environmentVariableReader)) + else { - return await IdStartsWithNsjAsync(packageIdPrefix, logger, queryUri, token); + return await IdStartsWithStjAsync(packageIdPrefix, includePrerelease, log, token); } + } + + private async Task> IdStartsWithStjAsync( + string packageIdPrefix, + bool includePrerelease, + Common.ILogger log, + CancellationToken token) + { + var queryUri = BuildQueryUri(packageIdPrefix, includePrerelease); + Common.ILogger logger = log ?? Common.NullLogger.Instance; AutoCompleteModel results = await _client.ProcessStreamAsync( new HttpSourceRequest(queryUri, logger), @@ -98,22 +92,22 @@ public override async Task> IdStartsWith( private async Task> IdStartsWithNsjAsync( string packageIdPrefix, - Common.ILogger logger, - Uri queryUri, + bool includePrerelease, + Common.ILogger log, CancellationToken token) { + var queryUri = BuildQueryUri(packageIdPrefix, includePrerelease); + Common.ILogger logger = log ?? Common.NullLogger.Instance; + var results = await _client.GetJObjectAsync( new HttpSourceRequest(queryUri, logger), logger, token); - token.ThrowIfCancellationRequested(); - if (results == null) { return Enumerable.Empty(); } - var data = results.Value("data"); if (data == null) { @@ -133,6 +127,25 @@ private async Task> IdStartsWithNsjAsync( return outputs.Where(item => item.StartsWith(packageIdPrefix, StringComparison.OrdinalIgnoreCase)); } + private Uri BuildQueryUri(string packageIdPrefix, bool includePrerelease) + { + var searchUrl = _serviceIndex.GetServiceEntryUri(ServiceTypes.SearchAutocompleteService); + + if (searchUrl == null) + { + throw new FatalProtocolException(Strings.Protocol_MissingSearchService); + } + + // Construct the query + var queryUrl = new UriBuilder(searchUrl.AbsoluteUri); + queryUrl.Query = + "q=" + WebUtility.UrlEncode(packageIdPrefix) + + "&prerelease=" + includePrerelease.ToString(CultureInfo.CurrentCulture).ToLowerInvariant() + + "&semVerLevel=2.0.0"; + + return queryUrl.Uri; + } + public override async Task> VersionStartsWith( string packageId, string versionPrefix, diff --git a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/AutoCompleteResourceV3Tests.cs b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/AutoCompleteResourceV3Tests.cs index 0a7a6c716ca..83ca6f4a351 100644 --- a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/AutoCompleteResourceV3Tests.cs +++ b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/AutoCompleteResourceV3Tests.cs @@ -22,21 +22,24 @@ public class AutoCompleteResourceV3Tests [InlineData("false")] // STJ path public async Task IdStartsWith_BothPaths_ReturnsResultsAsync(string useNsj) { + // Arrange var responses = new Dictionary(); - responses.Add("http://testsource.com/v3/index.json", JsonData.IndexWithoutFlatContainer); + responses.Add("http://testsource.test/v3/index.json", JsonData.IndexWithoutFlatContainer); responses.Add("https://api-v3search-0.nuget.org/autocomplete?q=newt&prerelease=true&semVerLevel=2.0.0", JsonData.AutoCompleteEndpointNewtResult); - var repo = StaticHttpHandler.CreateSource("http://testsource.com/v3/index.json", Repository.Provider.GetCoreV3(), responses); + var repo = StaticHttpHandler.CreateSource("http://testsource.test/v3/index.json", Repository.Provider.GetCoreV3(), responses); var resource = (AutoCompleteResourceV3)await repo.GetResourceAsync(CancellationToken.None); var envReader = new Mock(); - envReader.Setup(e => e.GetEnvironmentVariable(NuGetFeatureFlags.UseNSJDeserializationEnvVar)).Returns(useNsj); + envReader.Setup(e => e.GetEnvironmentVariable(NuGetFeatureFlags.UseLegacyJsonDeserializationEnvVar)).Returns(useNsj); var testResource = new AutoCompleteResourceV3(resource._client, resource._serviceIndex, resource._regResource, envReader.Object); - var logger = new TestLogger(); + + // Act var result = await testResource.IdStartsWith("newt", true, logger, CancellationToken.None); + // Assert Assert.Equal(10, result.Count()); Assert.NotEmpty(logger.Messages); } diff --git a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/NuGetFeatureFlagsTests.cs b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/NuGetFeatureFlagsTests.cs index 68e6bd7797c..434b12464da 100644 --- a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/NuGetFeatureFlagsTests.cs +++ b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/NuGetFeatureFlagsTests.cs @@ -11,27 +11,27 @@ namespace NuGet.Protocol.Tests public class NuGetFeatureFlagsTests { [Fact] - public void UseNSJDeserializationFeatureSwitch_Default_ReturnsFalse() + public void UseLegacyJsonDeserializationFeatureSwitch_Default_ReturnsFalse() { - Assert.False(NuGetFeatureFlags.UseNSJDeserializationFeatureSwitch); + Assert.False(NuGetFeatureFlags.UseLegacyJsonDeserializationFeatureSwitch); } [Fact] - public void IsNSJDeserializationEnabledByEnvironment_WhenEnvVarNotSet_ReturnsFalse() + public void IsLegacyJsonDeserializationEnabledByEnvironment_WhenEnvVarNotSet_ReturnsFalse() { - Assert.False(NuGetFeatureFlags.IsNSJDeserializationEnabledByEnvironment(TestEnvironmentVariableReader.EmptyInstance)); + Assert.False(NuGetFeatureFlags.IsLegacyJsonDeserializationEnabledByEnvironment(TestEnvironmentVariableReader.EmptyInstance)); } [Theory] [InlineData("true")] [InlineData("True")] [InlineData("TRUE")] - public void IsNSJDeserializationEnabledByEnvironment_WhenEnvVarSetToTrue_ReturnsTrue(string value) + public void IsLegacyJsonDeserializationEnabledByEnvironment_WhenEnvVarSetToTrue_ReturnsTrue(string value) { var env = new TestEnvironmentVariableReader( - new Dictionary { [NuGetFeatureFlags.UseNSJDeserializationEnvVar] = value }); + new Dictionary { [NuGetFeatureFlags.UseLegacyJsonDeserializationEnvVar] = value }); - Assert.True(NuGetFeatureFlags.IsNSJDeserializationEnabledByEnvironment(env)); + Assert.True(NuGetFeatureFlags.IsLegacyJsonDeserializationEnabledByEnvironment(env)); } [Theory] @@ -39,12 +39,12 @@ public void IsNSJDeserializationEnabledByEnvironment_WhenEnvVarSetToTrue_Returns [InlineData("0")] [InlineData("1")] [InlineData("anything")] - public void IsNSJDeserializationEnabledByEnvironment_WhenEnvVarSetToFalseOrUnrecognized_ReturnsFalse(string value) + public void IsLegacyJsonDeserializationEnabledByEnvironment_WhenEnvVarSetToFalseOrUnrecognized_ReturnsFalse(string value) { var env = new TestEnvironmentVariableReader( - new Dictionary { [NuGetFeatureFlags.UseNSJDeserializationEnvVar] = value }); + new Dictionary { [NuGetFeatureFlags.UseLegacyJsonDeserializationEnvVar] = value }); - Assert.False(NuGetFeatureFlags.IsNSJDeserializationEnabledByEnvironment(env)); + Assert.False(NuGetFeatureFlags.IsLegacyJsonDeserializationEnabledByEnvironment(env)); } } }