diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index d37206d6b411..0de35134714b 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -29,6 +29,7 @@ using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Testing; +using Microsoft.DotNet.RemoteExecutor; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; @@ -2903,6 +2904,44 @@ static void TestAction([AsParameters] ParameterListMixedRequiredStringsFromDiffe } #nullable enable + [ConditionalFact] + [RemoteExecutionSupported] + public void RequestDelegateFactory_WhenJsonIsReflectionEnabledByDefaultFalse() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault", false.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var @delegate = (string task) => new Todo(); + + // IsReflectionEnabledByDefault defaults to `false` when `PublishTrimmed=true`. For these scenarios, we + // expect users to configure JSON source generation as instructed in the `NotSupportedException` message. + var exception = Assert.Throws(() => RequestDelegateFactory.Create(@delegate)); + Assert.Contains("Microsoft.AspNetCore.Routing.Internal.RequestDelegateFactoryTests+Todo", exception.Message); + Assert.Contains("JsonSerializableAttribute", exception.Message); + }, options); + } + + [ConditionalFact] + [RemoteExecutionSupported] + public void RequestDelegateFactory_WhenJsonIsReflectionEnabledByDefaultTrue() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault", true.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var @delegate = (string task) => new Todo(); + + // Assert + var exception = Record.Exception(() => RequestDelegateFactory.Create(@delegate)); + Assert.Null(exception); + }, options); + } + private DefaultHttpContext CreateHttpContext() { var responseFeature = new TestHttpResponseFeature(); diff --git a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs index 273b11d6f65d..35000cb21ae2 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs @@ -23,6 +23,8 @@ public SystemTextJsonOutputFormatter(JsonSerializerOptions jsonSerializerOptions { SerializerOptions = jsonSerializerOptions; + // Use JsonTypeInfoResolver.Combine() to produce an empty TypeInfoResolver + jsonSerializerOptions.TypeInfoResolver ??= JsonTypeInfoResolver.Combine(); jsonSerializerOptions.MakeReadOnly(); SupportedEncodings.Add(Encoding.UTF8); diff --git a/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs index 203f03590831..d16ce19fb9da 100644 --- a/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs +++ b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs @@ -6,6 +6,8 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; +using Microsoft.AspNetCore.Testing; +using Microsoft.DotNet.RemoteExecutor; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -254,13 +256,39 @@ private static async IAsyncEnumerable GetPeopleAsync() } [Fact] - public void WriteResponseBodyAsync_Throws_WhenTypeResolverIsNull() + public void WriteResponseBodyAsync_Works_WhenTypeResolverIsNull() { // Arrange var jsonOptions = new JsonOptions(); jsonOptions.JsonSerializerOptions.TypeInfoResolver = null; - Assert.Throws(() => SystemTextJsonOutputFormatter.CreateFormatter(jsonOptions)); + var stjOutputFormatter = SystemTextJsonOutputFormatter.CreateFormatter(jsonOptions); + Assert.IsAssignableFrom(stjOutputFormatter.SerializerOptions.TypeInfoResolver); + } + + [ConditionalTheory] + [RemoteExecutionSupported] + [InlineData(false)] + [InlineData(true)] + public void STJOutputFormatter_UsesEmptyResolver_WhenJsonIsReflectionEnabledByDefaultFalse(bool isReflectionEnabledByDefault) + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault", isReflectionEnabledByDefault.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var jsonOptions = new JsonOptions(); + + // Assert + var stjOutputFormatter = SystemTextJsonOutputFormatter.CreateFormatter(jsonOptions); + Assert.IsAssignableFrom(stjOutputFormatter.SerializerOptions.TypeInfoResolver); + // Use default resolver if reflection is enabled instead of empty one + if (JsonSerializer.IsReflectionEnabledByDefault) + { + Assert.IsType(stjOutputFormatter.SerializerOptions.TypeInfoResolver); + } + }, options); } private class Person diff --git a/src/Shared/Json/JsonSerializerExtensions.cs b/src/Shared/Json/JsonSerializerExtensions.cs index 7068649a63f5..36f823efbd56 100644 --- a/src/Shared/Json/JsonSerializerExtensions.cs +++ b/src/Shared/Json/JsonSerializerExtensions.cs @@ -18,6 +18,8 @@ public static bool ShouldUseWith(this JsonTypeInfo jsonTypeInfo, [NotNullWhen(fa public static JsonTypeInfo GetReadOnlyTypeInfo(this JsonSerializerOptions options, Type type) { + // Use JsonTypeInfoResolver.Combine() to produce an empty TypeInfoResolver + options.TypeInfoResolver ??= JsonTypeInfoResolver.Combine(); options.MakeReadOnly(); return options.GetTypeInfo(type); }