From c2945c2ae51be788f86e986a1cef1e0d087149f2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 26 Jan 2023 14:58:31 -0800 Subject: [PATCH 01/48] Mvc JsonOptions updates --- .../MvcCoreJsonOptionsSetup.cs | 34 +++++++++++++++++++ .../MvcCoreServiceCollectionExtensions.cs | 2 ++ src/Mvc/Mvc.Core/src/JsonOptions.cs | 10 ------ 3 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreJsonOptionsSetup.cs diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreJsonOptionsSetup.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreJsonOptionsSetup.cs new file mode 100644 index 000000000000..838a9e42a7a6 --- /dev/null +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreJsonOptionsSetup.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization.Metadata; +using Microsoft.AspNetCore.Internal; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection; + +internal sealed class MvcCoreJsonOptionsSetup : IPostConfigureOptions +{ + public void PostConfigure(string? name, JsonOptions options) + { + if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) + { + InitializeForReflection(options); + } + } + + [RequiresUnreferencedCode("ABC")] + [RequiresDynamicCode("ABC")] + private static void InitializeForReflection(JsonOptions options) + { + if (options.JsonSerializerOptions.TypeInfoResolver is null) + { + options.JsonSerializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); + } + + // or + //options.JsonSerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(options.JsonSerializerOptions.TypeInfoResolver, new DefaultJsonTypeInfoResolver()); + } +} diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index a8178f76ca78..5384702496a6 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -139,6 +139,8 @@ internal static void AddMvcCoreServices(IServiceCollection services) ServiceDescriptor.Transient, ApiBehaviorOptionsSetup>()); services.TryAddEnumerable( ServiceDescriptor.Transient, MvcCoreRouteOptionsSetup>()); + services.TryAddEnumerable( + ServiceDescriptor.Transient, MvcCoreJsonOptionsSetup>()); // // Action Discovery diff --git a/src/Mvc/Mvc.Core/src/JsonOptions.cs b/src/Mvc/Mvc.Core/src/JsonOptions.cs index 44c2522924f2..d3966142f027 100644 --- a/src/Mvc/Mvc.Core/src/JsonOptions.cs +++ b/src/Mvc/Mvc.Core/src/JsonOptions.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text.Json; -using System.Text.Json.Serialization.Metadata; -using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -39,13 +37,5 @@ public class JsonOptions // from deserialization errors that might occur from deeply nested objects. // This value is the same for model binding and Json.Net's serialization. MaxDepth = MvcOptions.DefaultMaxModelBindingRecursionDepth, - - // The JsonSerializerOptions.GetTypeInfo method is called directly and needs a defined resolver - // setting the default resolver (reflection-based) but the user can overwrite it directly or calling - // .AddContext() - TypeInfoResolver = TrimmingAppContextSwitches.EnsureJsonTrimmability ? null : CreateDefaultTypeResolver() }; - - private static IJsonTypeInfoResolver CreateDefaultTypeResolver() - => new DefaultJsonTypeInfoResolver(); } From 26be191009a197400eeb5d98d585a3de0a83987a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 26 Jan 2023 14:58:55 -0800 Subject: [PATCH 02/48] Http JsonOptions updates --- .../src/DefaultHttpJsonOptionsSetup.cs | 39 +++++++++++++++++++ .../src/HttpJsonServiceExtensions.cs | 14 +++++++ src/Http/Http.Extensions/src/JsonOptions.cs | 14 ------- ...icrosoft.AspNetCore.Http.Extensions.csproj | 4 ++ .../src/PublicAPI.Unshipped.txt | 3 +- 5 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs diff --git a/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs b/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs new file mode 100644 index 000000000000..efc7adfaffde --- /dev/null +++ b/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization.Metadata; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.AspNetCore.Internal; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Http; + +internal sealed class DefaultHttpJsonOptionsSetup : IPostConfigureOptions +{ + public void PostConfigure(string? name, JsonOptions options) + { + if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) + { +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. + InitializeForReflection(options); +#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. +#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + } + } + + [RequiresUnreferencedCode("TODO")] + [RequiresDynamicCode("TODO")] + private static void InitializeForReflection(JsonOptions options) + { + if (options.SerializerOptions.TypeInfoResolver is null) + { + options.SerializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); + } + + + // or + //options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(options.SerializerOptions.TypeInfoResolver, new DefaultJsonTypeInfoResolver()); + } +} diff --git a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs index c5ad6b8071bc..03cf192a41d4 100644 --- a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs @@ -1,7 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Json; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection; @@ -25,4 +28,15 @@ public static IServiceCollection ConfigureHttpJsonOptions(this IServiceCollectio services.Configure(configureOptions); return services; } + + /// + /// + /// + /// + /// + public static IServiceCollection ConfigureDefaultHttpJsonOptions(this IServiceCollection services) + { + services.TryAddEnumerable(ServiceDescriptor.Singleton, DefaultHttpJsonOptionsSetup>()); + return services; + } } diff --git a/src/Http/Http.Extensions/src/JsonOptions.cs b/src/Http/Http.Extensions/src/JsonOptions.cs index 5ca45dc9faff..4b4296d38584 100644 --- a/src/Http/Http.Extensions/src/JsonOptions.cs +++ b/src/Http/Http.Extensions/src/JsonOptions.cs @@ -3,8 +3,6 @@ using System.Text.Encodings.Web; using System.Text.Json; -using System.Text.Json.Serialization.Metadata; -using Microsoft.AspNetCore.Internal; #nullable enable @@ -23,11 +21,6 @@ public class JsonOptions // Because these options are for producing content that is written directly to the request // (and not embedded in an HTML page for example), we can use UnsafeRelaxedJsonEscaping. Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - - // The JsonSerializerOptions.GetTypeInfo method is called directly and needs a defined resolver - // setting the default resolver (reflection-based) but the user can overwrite it directly or calling - // .AddContext() - TypeInfoResolver = TrimmingAppContextSwitches.EnsureJsonTrimmability ? null : CreateDefaultTypeResolver() }; // Use a copy so the defaults are not modified. @@ -35,11 +28,4 @@ public class JsonOptions /// Gets the . /// public JsonSerializerOptions SerializerOptions { get; internal set; } = new JsonSerializerOptions(DefaultSerializerOptions); - -#pragma warning disable IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml -#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. - private static IJsonTypeInfoResolver CreateDefaultTypeResolver() - => new DefaultJsonTypeInfoResolver(); -#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. -#pragma warning restore IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml } diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj index 9ac6d360d2b7..483b75768e13 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj @@ -26,6 +26,10 @@ + + + + diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index 393817af3895..e659eaa5bf25 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ #nullable enable static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, object? value, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, string? contentType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! \ No newline at end of file +static Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, object? value, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, string? contentType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +static Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.ConfigureDefaultHttpJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! \ No newline at end of file From b6fadd972f5dc88ae65a8058a913d532e4ae7fa1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 26 Jan 2023 15:00:33 -0800 Subject: [PATCH 03/48] Bad merge --- .../src/Microsoft.AspNetCore.Http.Extensions.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj index 2703a377837c..483b75768e13 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj @@ -25,10 +25,6 @@ - - - - From d31a8e2a5759f0406b8d298dba76957771de2211 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 26 Jan 2023 15:04:19 -0800 Subject: [PATCH 04/48] fix end line --- src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index e659eaa5bf25..f05660c24fd8 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,4 +1,4 @@ #nullable enable static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, object? value, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, string? contentType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.ConfigureDefaultHttpJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! \ No newline at end of file +static Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.ConfigureDefaultHttpJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! From cc71ed29e1b970647320a825c2157b02cf4470af Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 26 Jan 2023 15:37:19 -0800 Subject: [PATCH 05/48] Updating RUC/RDC message --- src/DefaultBuilder/src/WebHost.cs | 2 ++ src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs | 5 ++--- .../src/DependencyInjection/MvcCoreJsonOptionsSetup.cs | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/DefaultBuilder/src/WebHost.cs b/src/DefaultBuilder/src/WebHost.cs index 4a49c3ef8e3c..ebfadc647035 100644 --- a/src/DefaultBuilder/src/WebHost.cs +++ b/src/DefaultBuilder/src/WebHost.cs @@ -228,6 +228,8 @@ internal static void ConfigureWebDefaults(IWebHostBuilder builder) builder .UseIIS() .UseIISIntegration(); + + builder.ConfigureServices(services => services.ConfigureDefaultHttpJsonOptions()); } internal static void UseKestrel(IWebHostBuilder builder) diff --git a/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs b/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs index efc7adfaffde..0dfad5d9cb36 100644 --- a/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs +++ b/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs @@ -23,8 +23,8 @@ public void PostConfigure(string? name, JsonOptions options) } } - [RequiresUnreferencedCode("TODO")] - [RequiresDynamicCode("TODO")] + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] private static void InitializeForReflection(JsonOptions options) { if (options.SerializerOptions.TypeInfoResolver is null) @@ -32,7 +32,6 @@ private static void InitializeForReflection(JsonOptions options) options.SerializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); } - // or //options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(options.SerializerOptions.TypeInfoResolver, new DefaultJsonTypeInfoResolver()); } diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreJsonOptionsSetup.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreJsonOptionsSetup.cs index 838a9e42a7a6..d254b8742f30 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreJsonOptionsSetup.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreJsonOptionsSetup.cs @@ -19,8 +19,8 @@ public void PostConfigure(string? name, JsonOptions options) } } - [RequiresUnreferencedCode("ABC")] - [RequiresDynamicCode("ABC")] + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] private static void InitializeForReflection(JsonOptions options) { if (options.JsonSerializerOptions.TypeInfoResolver is null) From f47a07617901af708243b956bc8a7a31e3aec95a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 26 Jan 2023 22:00:56 -0800 Subject: [PATCH 06/48] Add call to AddRouting --- src/DefaultBuilder/src/WebHost.cs | 2 -- .../src/DefaultHttpJsonOptionsSetup.cs | 22 ++++--------------- .../src/HttpJsonServiceExtensions.cs | 11 +++++++++- .../RoutingServiceCollectionExtensions.cs | 3 +++ .../MvcCoreJsonOptionsSetup.cs | 18 ++++----------- .../MvcCoreServiceCollectionExtensions.cs | 9 ++++++-- 6 files changed, 28 insertions(+), 37 deletions(-) diff --git a/src/DefaultBuilder/src/WebHost.cs b/src/DefaultBuilder/src/WebHost.cs index ebfadc647035..4a49c3ef8e3c 100644 --- a/src/DefaultBuilder/src/WebHost.cs +++ b/src/DefaultBuilder/src/WebHost.cs @@ -228,8 +228,6 @@ internal static void ConfigureWebDefaults(IWebHostBuilder builder) builder .UseIIS() .UseIISIntegration(); - - builder.ConfigureServices(services => services.ConfigureDefaultHttpJsonOptions()); } internal static void UseKestrel(IWebHostBuilder builder) diff --git a/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs b/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs index 0dfad5d9cb36..3046570eb759 100644 --- a/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs +++ b/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs @@ -4,35 +4,21 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Http.Json; -using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Http; +[RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] +[RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] internal sealed class DefaultHttpJsonOptionsSetup : IPostConfigureOptions { public void PostConfigure(string? name, JsonOptions options) { - if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) - { -#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code -#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. - InitializeForReflection(options); -#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. -#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code - } + InitializeForReflection(options); } - [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] - [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] private static void InitializeForReflection(JsonOptions options) { - if (options.SerializerOptions.TypeInfoResolver is null) - { - options.SerializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); - } - - // or - //options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(options.SerializerOptions.TypeInfoResolver, new DefaultJsonTypeInfoResolver()); + options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(options.SerializerOptions.TypeInfoResolver, new DefaultJsonTypeInfoResolver()); } } diff --git a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs index 03cf192a41d4..01e3d54f053e 100644 --- a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Json; +using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; @@ -36,7 +37,15 @@ public static IServiceCollection ConfigureHttpJsonOptions(this IServiceCollectio /// public static IServiceCollection ConfigureDefaultHttpJsonOptions(this IServiceCollection services) { - services.TryAddEnumerable(ServiceDescriptor.Singleton, DefaultHttpJsonOptionsSetup>()); + if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) + { +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. + services.TryAddEnumerable(ServiceDescriptor.Singleton, DefaultHttpJsonOptionsSetup>()); +#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. +#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + } + return services; } } diff --git a/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs index 6c912748bca5..b13b868557a1 100644 --- a/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs +++ b/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs @@ -98,6 +98,9 @@ public static IServiceCollection AddRouting(this IServiceCollection services) // Set RouteHandlerOptions.ThrowOnBadRequest in development services.TryAddEnumerable(ServiceDescriptor.Transient, ConfigureRouteHandlerOptions>()); + // JsonOptions + services.ConfigureDefaultHttpJsonOptions(); + return services; } diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreJsonOptionsSetup.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreJsonOptionsSetup.cs index d254b8742f30..32cc77cf19c9 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreJsonOptionsSetup.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreJsonOptionsSetup.cs @@ -3,32 +3,22 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization.Metadata; -using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection; +[RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] +[RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] internal sealed class MvcCoreJsonOptionsSetup : IPostConfigureOptions { public void PostConfigure(string? name, JsonOptions options) { - if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) - { - InitializeForReflection(options); - } + InitializeForReflection(options); } - [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] - [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] private static void InitializeForReflection(JsonOptions options) { - if (options.JsonSerializerOptions.TypeInfoResolver is null) - { - options.JsonSerializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); - } - - // or - //options.JsonSerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(options.JsonSerializerOptions.TypeInfoResolver, new DefaultJsonTypeInfoResolver()); + options.JsonSerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(options.JsonSerializerOptions.TypeInfoResolver, new DefaultJsonTypeInfoResolver()); } } diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 5384702496a6..3709890be47b 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using System.Linq; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; @@ -139,8 +140,12 @@ internal static void AddMvcCoreServices(IServiceCollection services) ServiceDescriptor.Transient, ApiBehaviorOptionsSetup>()); services.TryAddEnumerable( ServiceDescriptor.Transient, MvcCoreRouteOptionsSetup>()); - services.TryAddEnumerable( - ServiceDescriptor.Transient, MvcCoreJsonOptionsSetup>()); + + if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) + { + services.TryAddEnumerable( + ServiceDescriptor.Transient, MvcCoreJsonOptionsSetup>()); + } // // Action Discovery From d227b30b89cf8cd0564b20abeec6ea26954a583a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 27 Jan 2023 14:14:05 -0800 Subject: [PATCH 07/48] Fix suppressions --- src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs | 4 ++-- ...osoft.AspNetCore.Http.Extensions.WarningSuppressions.xml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs index 01e3d54f053e..e2fda3ca11ed 100644 --- a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs @@ -39,11 +39,11 @@ public static IServiceCollection ConfigureDefaultHttpJsonOptions(this IServiceCo { if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) { -#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +#pragma warning disable IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml #pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. services.TryAddEnumerable(ServiceDescriptor.Singleton, DefaultHttpJsonOptionsSetup>()); #pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. -#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +#pragma warning restore IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml } return services; diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml index 70f9e571648c..92936a7a2b31 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml @@ -1,12 +1,12 @@ - + ILLink IL2026 member - M:Microsoft.AspNetCore.Http.Json.JsonOptions.CreateDefaultTypeResolver - This warning is left in the product so developers get an ILLink warning when trimming an app, in future, only when Microsoft.AspNetCore.EnsureJsonTrimmability=false. + M:Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.ConfigureDefaultHttpJsonOptions(Microsoft.Extensions.DependencyInjection.IServiceCollection) + This warning is left in the product so developers get an ILLink warning when trimming an app only when Microsoft.AspNetCore.EnsureJsonTrimmability=false. From df207a0940ac882475ba186a458d190189c2bbfc Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 27 Jan 2023 14:16:29 -0800 Subject: [PATCH 08/48] Remove empty spaces --- ...Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml index 92936a7a2b31..d940a7c5283d 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml @@ -5,7 +5,7 @@ ILLink IL2026 member - M:Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.ConfigureDefaultHttpJsonOptions(Microsoft.Extensions.DependencyInjection.IServiceCollection) + M:Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.ConfigureDefaultHttpJsonOptions(Microsoft.Extensions.DependencyInjection.IServiceCollection) This warning is left in the product so developers get an ILLink warning when trimming an app only when Microsoft.AspNetCore.EnsureJsonTrimmability=false. From 9e9794171d954c75cd0ce3bd065cf7f077b90b71 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 27 Jan 2023 16:49:53 -0800 Subject: [PATCH 09/48] Moving to Routing --- .../src/HttpJsonServiceExtensions.cs | 23 ------------------- ...re.Http.Extensions.WarningSuppressions.xml | 5 ++-- ...icrosoft.AspNetCore.Http.Extensions.csproj | 7 +----- .../src/PublicAPI.Unshipped.txt | 1 - .../src/ConfigureHttpJsonOptions.cs} | 4 ++-- .../RoutingServiceCollectionExtensions.cs | 11 ++++++++- ...AspNetCore.Routing.WarningSuppressions.xml | 11 +++++++++ .../src/Microsoft.AspNetCore.Routing.csproj | 9 ++++++-- .../src/Properties/ILLink.Substitutions.xml | 2 +- 9 files changed, 34 insertions(+), 39 deletions(-) rename src/Http/{Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs => Routing/src/ConfigureHttpJsonOptions.cs} (89%) create mode 100644 src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml rename src/Http/{Http.Extensions => Routing}/src/Properties/ILLink.Substitutions.xml (84%) diff --git a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs index e2fda3ca11ed..c5ad6b8071bc 100644 --- a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs @@ -1,11 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Json; -using Microsoft.AspNetCore.Internal; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection; @@ -29,23 +25,4 @@ public static IServiceCollection ConfigureHttpJsonOptions(this IServiceCollectio services.Configure(configureOptions); return services; } - - /// - /// - /// - /// - /// - public static IServiceCollection ConfigureDefaultHttpJsonOptions(this IServiceCollection services) - { - if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) - { -#pragma warning disable IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml -#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. - services.TryAddEnumerable(ServiceDescriptor.Singleton, DefaultHttpJsonOptionsSetup>()); -#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. -#pragma warning restore IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml - } - - return services; - } } diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml index d940a7c5283d..65698062cc0f 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml @@ -1,4 +1,4 @@ - + @@ -6,7 +6,6 @@ IL2026 member M:Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.ConfigureDefaultHttpJsonOptions(Microsoft.Extensions.DependencyInjection.IServiceCollection) - This warning is left in the product so developers get an ILLink warning when trimming an app only when Microsoft.AspNetCore.EnsureJsonTrimmability=false. - + \ No newline at end of file diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj index 483b75768e13..eecc04ece71e 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj @@ -23,12 +23,7 @@ - - - - - - + diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index f05660c24fd8..254e8dfb2161 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,4 +1,3 @@ #nullable enable static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, object? value, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, string? contentType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.ConfigureDefaultHttpJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs b/src/Http/Routing/src/ConfigureHttpJsonOptions.cs similarity index 89% rename from src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs rename to src/Http/Routing/src/ConfigureHttpJsonOptions.cs index 3046570eb759..4b34aa20928d 100644 --- a/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs +++ b/src/Http/Routing/src/ConfigureHttpJsonOptions.cs @@ -6,11 +6,11 @@ using Microsoft.AspNetCore.Http.Json; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Http; +namespace Microsoft.AspNetCore.Routing; [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] -internal sealed class DefaultHttpJsonOptionsSetup : IPostConfigureOptions +internal sealed class ConfigureHttpJsonOptions : IPostConfigureOptions { public void PostConfigure(string? name, JsonOptions options) { diff --git a/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs index b13b868557a1..cdccfd990a07 100644 --- a/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs +++ b/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.ObjectModel; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Matching; @@ -99,7 +101,14 @@ public static IServiceCollection AddRouting(this IServiceCollection services) services.TryAddEnumerable(ServiceDescriptor.Transient, ConfigureRouteHandlerOptions>()); // JsonOptions - services.ConfigureDefaultHttpJsonOptions(); + if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) + { +#pragma warning disable IL2026 // Suppressed in Microsoft.AspNetCore.Routing.WarningSuppressions.xml +#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. + services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureHttpJsonOptions>()); +#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. +#pragma warning restore IL2026 // Suppressed in Microsoft.AspNetCore.Routing.WarningSuppressions.xml + } return services; } diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml new file mode 100644 index 000000000000..40ec94982641 --- /dev/null +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml @@ -0,0 +1,11 @@ + + + + + ILLink + IL2026 + member + M:Microsoft.Extensions.DependencyInjection.RoutingServiceCollectionExtensions.AddRouting(Microsoft.Extensions.DependencyInjection.IServiceCollection) + + + \ No newline at end of file diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index bfa84d35edb5..f5637e931e14 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -31,8 +31,13 @@ - - + + + + + + + diff --git a/src/Http/Http.Extensions/src/Properties/ILLink.Substitutions.xml b/src/Http/Routing/src/Properties/ILLink.Substitutions.xml similarity index 84% rename from src/Http/Http.Extensions/src/Properties/ILLink.Substitutions.xml rename to src/Http/Routing/src/Properties/ILLink.Substitutions.xml index 92c77d69a9e6..3beb4a06f235 100644 --- a/src/Http/Http.Extensions/src/Properties/ILLink.Substitutions.xml +++ b/src/Http/Routing/src/Properties/ILLink.Substitutions.xml @@ -1,6 +1,6 @@ - + From eb64558d9173db93e934ce7b930f50dbb6bf0f5c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 29 Jan 2023 22:09:02 -0800 Subject: [PATCH 10/48] Revert "Moving to Routing" This reverts commit 9e9794171d954c75cd0ce3bd065cf7f077b90b71. --- .../src/DefaultHttpJsonOptionsSetup.cs} | 4 ++-- .../src/HttpJsonServiceExtensions.cs | 23 +++++++++++++++++++ ...re.Http.Extensions.WarningSuppressions.xml | 5 ++-- ...icrosoft.AspNetCore.Http.Extensions.csproj | 7 +++++- .../src/Properties/ILLink.Substitutions.xml | 2 +- .../src/PublicAPI.Unshipped.txt | 1 + .../RoutingServiceCollectionExtensions.cs | 11 +-------- ...AspNetCore.Routing.WarningSuppressions.xml | 11 --------- .../src/Microsoft.AspNetCore.Routing.csproj | 9 ++------ 9 files changed, 39 insertions(+), 34 deletions(-) rename src/Http/{Routing/src/ConfigureHttpJsonOptions.cs => Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs} (89%) rename src/Http/{Routing => Http.Extensions}/src/Properties/ILLink.Substitutions.xml (84%) delete mode 100644 src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml diff --git a/src/Http/Routing/src/ConfigureHttpJsonOptions.cs b/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs similarity index 89% rename from src/Http/Routing/src/ConfigureHttpJsonOptions.cs rename to src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs index 4b34aa20928d..3046570eb759 100644 --- a/src/Http/Routing/src/ConfigureHttpJsonOptions.cs +++ b/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs @@ -6,11 +6,11 @@ using Microsoft.AspNetCore.Http.Json; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Routing; +namespace Microsoft.AspNetCore.Http; [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] -internal sealed class ConfigureHttpJsonOptions : IPostConfigureOptions +internal sealed class DefaultHttpJsonOptionsSetup : IPostConfigureOptions { public void PostConfigure(string? name, JsonOptions options) { diff --git a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs index c5ad6b8071bc..e2fda3ca11ed 100644 --- a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs @@ -1,7 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Json; +using Microsoft.AspNetCore.Internal; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection; @@ -25,4 +29,23 @@ public static IServiceCollection ConfigureHttpJsonOptions(this IServiceCollectio services.Configure(configureOptions); return services; } + + /// + /// + /// + /// + /// + public static IServiceCollection ConfigureDefaultHttpJsonOptions(this IServiceCollection services) + { + if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) + { +#pragma warning disable IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml +#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. + services.TryAddEnumerable(ServiceDescriptor.Singleton, DefaultHttpJsonOptionsSetup>()); +#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. +#pragma warning restore IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml + } + + return services; + } } diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml index 65698062cc0f..d940a7c5283d 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml @@ -1,4 +1,4 @@ - + @@ -6,6 +6,7 @@ IL2026 member M:Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.ConfigureDefaultHttpJsonOptions(Microsoft.Extensions.DependencyInjection.IServiceCollection) + This warning is left in the product so developers get an ILLink warning when trimming an app only when Microsoft.AspNetCore.EnsureJsonTrimmability=false. - \ No newline at end of file + diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj index eecc04ece71e..483b75768e13 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj @@ -23,7 +23,12 @@ - + + + + + + diff --git a/src/Http/Routing/src/Properties/ILLink.Substitutions.xml b/src/Http/Http.Extensions/src/Properties/ILLink.Substitutions.xml similarity index 84% rename from src/Http/Routing/src/Properties/ILLink.Substitutions.xml rename to src/Http/Http.Extensions/src/Properties/ILLink.Substitutions.xml index 3beb4a06f235..92c77d69a9e6 100644 --- a/src/Http/Routing/src/Properties/ILLink.Substitutions.xml +++ b/src/Http/Http.Extensions/src/Properties/ILLink.Substitutions.xml @@ -1,6 +1,6 @@ - + diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index 254e8dfb2161..f05660c24fd8 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ #nullable enable static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, object? value, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, string? contentType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +static Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.ConfigureDefaultHttpJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs index cdccfd990a07..b13b868557a1 100644 --- a/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs +++ b/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.ObjectModel; -using Microsoft.AspNetCore.Http.Json; -using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Matching; @@ -101,14 +99,7 @@ public static IServiceCollection AddRouting(this IServiceCollection services) services.TryAddEnumerable(ServiceDescriptor.Transient, ConfigureRouteHandlerOptions>()); // JsonOptions - if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) - { -#pragma warning disable IL2026 // Suppressed in Microsoft.AspNetCore.Routing.WarningSuppressions.xml -#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. - services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureHttpJsonOptions>()); -#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. -#pragma warning restore IL2026 // Suppressed in Microsoft.AspNetCore.Routing.WarningSuppressions.xml - } + services.ConfigureDefaultHttpJsonOptions(); return services; } diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml deleted file mode 100644 index 40ec94982641..000000000000 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - ILLink - IL2026 - member - M:Microsoft.Extensions.DependencyInjection.RoutingServiceCollectionExtensions.AddRouting(Microsoft.Extensions.DependencyInjection.IServiceCollection) - - - \ No newline at end of file diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index f5637e931e14..bfa84d35edb5 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -31,13 +31,8 @@ - - - - - - - + + From 5b175ed9450a05d28265689037a690cbae8072d5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 30 Jan 2023 13:50:20 -0800 Subject: [PATCH 11/48] Adding AddDefaultJsonOptions API --- src/DefaultBuilder/src/WebHost.cs | 4 ++ .../src/HttpJsonServiceExtensions.cs | 11 ++-- ...re.Http.Extensions.WarningSuppressions.xml | 2 +- .../src/PublicAPI.Unshipped.txt | 2 +- .../test/HttpRequestJsonExtensionsTests.cs | 37 +++++++++----- .../test/HttpResponseJsonExtensionsTests.cs | 51 +++++++++++-------- .../test/ProblemDetailsDefaultWriterTest.cs | 7 ++- .../test/ProblemDetailsServiceTest.cs | 5 ++ .../test/RequestDelegateFactoryTests.cs | 13 ++--- .../RoutingServiceCollectionExtensions.cs | 3 -- .../test/FunctionalTests/RouteHandlerTest.cs | 1 + ...egateEndpointRouteBuilderExtensionsTest.cs | 9 ++++ 12 files changed, 95 insertions(+), 50 deletions(-) diff --git a/src/DefaultBuilder/src/WebHost.cs b/src/DefaultBuilder/src/WebHost.cs index 4a49c3ef8e3c..5563b69c31ef 100644 --- a/src/DefaultBuilder/src/WebHost.cs +++ b/src/DefaultBuilder/src/WebHost.cs @@ -258,6 +258,10 @@ internal static void UseKestrel(IWebHostBuilder builder) services.AddTransient, ForwardedHeadersOptionsSetup>(); services.AddRouting(); + + // JsonOptions + services.AddDefaultHttpJsonOptions(); + }); } diff --git a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs index e2fda3ca11ed..4c7d25ab3c6d 100644 --- a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs @@ -26,8 +26,9 @@ public static class HttpJsonServiceExtensions /// The modified . public static IServiceCollection ConfigureHttpJsonOptions(this IServiceCollection services, Action configureOptions) { - services.Configure(configureOptions); - return services; + services.Configure(configureOptions); + + return services.AddDefaultHttpJsonOptions(); } /// @@ -35,8 +36,12 @@ public static IServiceCollection ConfigureHttpJsonOptions(this IServiceCollectio /// /// /// - public static IServiceCollection ConfigureDefaultHttpJsonOptions(this IServiceCollection services) + public static IServiceCollection AddDefaultHttpJsonOptions(this IServiceCollection services) { + ArgumentNullException.ThrowIfNull(services); + + services.AddOptions(); + if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) { #pragma warning disable IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml index d940a7c5283d..db9409dec8da 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml @@ -5,7 +5,7 @@ ILLink IL2026 member - M:Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.ConfigureDefaultHttpJsonOptions(Microsoft.Extensions.DependencyInjection.IServiceCollection) + M:Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.AddDefaultHttpJsonOptions(Microsoft.Extensions.DependencyInjection.IServiceCollection) This warning is left in the product so developers get an ILLink warning when trimming an app only when Microsoft.AspNetCore.EnsureJsonTrimmability=false. diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index f05660c24fd8..1a9348da333d 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,4 +1,4 @@ #nullable enable static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, object? value, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, string? contentType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.ConfigureDefaultHttpJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.AddDefaultHttpJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs b/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs index 4d63107214e9..8c2031357cb9 100644 --- a/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs +++ b/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs @@ -4,6 +4,7 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using Microsoft.Extensions.DependencyInjection; #nullable enable @@ -35,7 +36,7 @@ public void HasJsonContentType(string contentType, bool hasJsonContentType) public async Task ReadFromJsonAsyncGeneric_NonJsonContentType_ThrowError() { // Arrange - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Request.ContentType = "text/json"; // Act @@ -50,7 +51,7 @@ public async Task ReadFromJsonAsyncGeneric_NonJsonContentType_ThrowError() public async Task ReadFromJsonAsyncGeneric_NoBodyContent_ThrowError() { // Arrange - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Request.ContentType = "application/json"; // Act @@ -65,7 +66,7 @@ public async Task ReadFromJsonAsyncGeneric_NoBodyContent_ThrowError() public async Task ReadFromJsonAsyncGeneric_ValidBodyContent_ReturnValue() { // Arrange - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Request.ContentType = "application/json"; context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("1")); @@ -80,7 +81,7 @@ public async Task ReadFromJsonAsyncGeneric_ValidBodyContent_ReturnValue() public async Task ReadFromJsonAsyncGeneric_WithOptions_ReturnValue() { // Arrange - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Request.ContentType = "application/json"; context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("[1,2,]")); @@ -101,7 +102,7 @@ public async Task ReadFromJsonAsyncGeneric_WithOptions_ReturnValue() public async Task ReadFromJsonAsyncGeneric_Utf8Encoding_ReturnValue() { // Arrange - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Request.ContentType = "application/json; charset=utf-8"; context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("[1,2]")); @@ -119,7 +120,7 @@ public async Task ReadFromJsonAsyncGeneric_Utf8Encoding_ReturnValue() public async Task ReadFromJsonAsyncGeneric_Utf16Encoding_ReturnValue() { // Arrange - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Request.ContentType = "application/json; charset=utf-16"; context.Request.Body = new MemoryStream(Encoding.Unicode.GetBytes(@"{""name"": ""激光這兩個字是甚麼意思""}")); @@ -134,7 +135,7 @@ public async Task ReadFromJsonAsyncGeneric_Utf16Encoding_ReturnValue() public async Task ReadFromJsonAsyncGeneric_WithCancellationToken_CancellationRaised() { // Arrange - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Request.ContentType = "application /json"; context.Request.Body = new TestStream(); @@ -154,7 +155,7 @@ public async Task ReadFromJsonAsyncGeneric_WithCancellationToken_CancellationRai public async Task ReadFromJsonAsyncGeneric_InvalidEncoding_ThrowError() { // Arrange - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Request.ContentType = "application/json; charset=invalid"; // Act @@ -168,7 +169,7 @@ public async Task ReadFromJsonAsyncGeneric_InvalidEncoding_ThrowError() public async Task ReadFromJsonAsync_ValidBodyContent_ReturnValue() { // Arrange - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Request.ContentType = "application/json"; context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("1")); @@ -183,7 +184,7 @@ public async Task ReadFromJsonAsync_ValidBodyContent_ReturnValue() public async Task ReadFromJsonAsync_Utf16Encoding_ReturnValue() { // Arrange - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Request.ContentType = "application/json; charset=utf-16"; context.Request.Body = new MemoryStream(Encoding.Unicode.GetBytes(@"{""name"": ""激光這兩個字是甚麼意思""}")); @@ -198,7 +199,7 @@ public async Task ReadFromJsonAsync_Utf16Encoding_ReturnValue() public async Task ReadFromJsonAsync_InvalidEncoding_ThrowError() { // Arrange - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Request.ContentType = "application/json; charset=invalid"; // Act @@ -212,7 +213,7 @@ public async Task ReadFromJsonAsync_InvalidEncoding_ThrowError() public async Task ReadFromJsonAsync_WithOptions_ReturnValue() { // Arrange - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Request.ContentType = "application/json"; context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("[1,2,]")); @@ -233,7 +234,7 @@ public async Task ReadFromJsonAsync_WithOptions_ReturnValue() public async Task ReadFromJsonAsync_WithTypeInfo_ReturnValue() { // Arrange - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Request.ContentType = "application/json"; context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("[1,2,]")); @@ -255,7 +256,7 @@ public async Task ReadFromJsonAsync_WithTypeInfo_ReturnValue() public async Task ReadFromJsonAsync_WithGenericTypeInfo_ReturnValue() { // Arrange - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Request.ContentType = "application/json"; context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("[1,2,]")); @@ -273,4 +274,12 @@ public async Task ReadFromJsonAsync_WithGenericTypeInfo_ReturnValue() i => Assert.Equal(1, i), i => Assert.Equal(2, i)); } + + private DefaultHttpContext CreateHttpContext() + { + var services = new ServiceCollection(); + services.AddDefaultHttpJsonOptions(); + + return new() { RequestServices = services.BuildServiceProvider() }; + } } diff --git a/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs b/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs index 9562ee94c099..8baa62e1e0f1 100644 --- a/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs +++ b/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs @@ -7,6 +7,7 @@ using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; #nullable enable @@ -19,7 +20,7 @@ public async Task WriteAsJsonAsyncGeneric_SimpleValue_JsonResponse() { // Arrange var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; // Act @@ -38,7 +39,7 @@ public async Task WriteAsJsonAsyncGeneric_NullValue_JsonResponse() { // Arrange var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; // Act @@ -56,7 +57,7 @@ public async Task WriteAsJsonAsyncGeneric_WithOptions_JsonResponse() { // Arrange var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; // Act @@ -89,7 +90,7 @@ public async Task WriteAsJsonAsyncGeneric_CustomStatusCode_StatusCodeUnchanged() { // Arrange var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; // Act @@ -106,7 +107,7 @@ public async Task WriteAsJsonAsyncGeneric_WithContentType_JsonResponseWithCustom { // Arrange var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; // Act @@ -120,7 +121,7 @@ public async Task WriteAsJsonAsyncGeneric_WithContentType_JsonResponseWithCustom public async Task WriteAsJsonAsyncGeneric_WithCancellationToken_CancellationRaised() { // Arrange - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = new TestStream(); var cts = new CancellationTokenSource(); @@ -140,7 +141,7 @@ public async Task WriteAsJsonAsyncGeneric_ObjectWithStrings_CamcelCaseAndNotEsca { // Arrange var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; var value = new TestObject { @@ -160,7 +161,7 @@ public async Task WriteAsJsonAsync_SimpleValue_JsonResponse() { // Arrange var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; // Act @@ -179,7 +180,7 @@ public async Task WriteAsJsonAsync_NullValue_JsonResponse() { // Arrange var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; // Act @@ -197,7 +198,7 @@ public async Task WriteAsJsonAsync_NullType_ThrowsArgumentNullException() { // Arrange var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; // Act & Assert @@ -209,7 +210,7 @@ public async Task WriteAsJsonAsync_NullResponse_ThrowsArgumentNullException() { // Arrange var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; // Act & Assert @@ -221,7 +222,7 @@ public async Task WriteAsJsonAsync_ObjectWithStrings_CamcelCaseAndNotEscaped() { // Arrange var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; var value = new TestObject { @@ -241,7 +242,7 @@ public async Task WriteAsJsonAsync_CustomStatusCode_StatusCodeUnchanged() { // Arrange var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; // Act @@ -258,7 +259,7 @@ public async Task WriteAsJsonAsyncGeneric_AsyncEnumerable() { // Arrange var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; // Act @@ -283,7 +284,7 @@ public async Task WriteAsJsonAsync_AsyncEnumerable() { // Arrange var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; // Act @@ -309,7 +310,7 @@ public async Task WriteAsJsonAsyncGeneric_AsyncEnumerable_ClosedConnecton() // Arrange var cts = new CancellationTokenSource(); var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; context.RequestAborted = cts.Token; var iterated = false; @@ -343,7 +344,7 @@ public async Task WriteAsJsonAsync_AsyncEnumerable_ClosedConnecton() // Arrange var cts = new CancellationTokenSource(); var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; context.RequestAborted = cts.Token; var iterated = false; @@ -376,7 +377,7 @@ public async Task WriteAsJsonAsync_AsyncEnumerable_UserPassedTokenThrows() { // Arrange var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; context.RequestAborted = new CancellationToken(canceled: true); var cts = new CancellationTokenSource(); @@ -410,7 +411,7 @@ public async Task WriteAsJsonAsyncGeneric_AsyncEnumerable_UserPassedTokenThrows( { // Arrange var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; context.RequestAborted = new CancellationToken(canceled: true); var cts = new CancellationTokenSource(); @@ -444,7 +445,7 @@ public async Task WriteAsJsonAsyncGeneric_WithJsonTypeInfo_JsonResponse() { // Arrange var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; // Act @@ -465,7 +466,7 @@ public async Task WriteAsJsonAsync_NullValue_WithJsonTypeInfo_JsonResponse() { // Arrange var body = new MemoryStream(); - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); context.Response.Body = body; // Act @@ -481,6 +482,14 @@ public async Task WriteAsJsonAsync_NullValue_WithJsonTypeInfo_JsonResponse() Assert.Equal("null", data); } + private static DefaultHttpContext CreateHttpContext() + { + var services = new ServiceCollection(); + services.AddDefaultHttpJsonOptions(); + + return new() { RequestServices = services.BuildServiceProvider() }; + } + public class TestObject { public string? StringProperty { get; set; } diff --git a/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs index 43435af6e8c5..1bc006fd0d9f 100644 --- a/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs @@ -558,7 +558,12 @@ private static IServiceProvider CreateServices() private static DefaultProblemDetailsWriter GetWriter(ProblemDetailsOptions options = null, JsonOptions jsonOptions = null) { options ??= new ProblemDetailsOptions(); - jsonOptions ??= new JsonOptions(); + + if (jsonOptions is null) + { + jsonOptions = new JsonOptions(); + jsonOptions.SerializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); + } return new DefaultProblemDetailsWriter(Options.Create(options), Options.Create(jsonOptions)); } diff --git a/src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs index 66abd0de36a3..7cbfbbba6e98 100644 --- a/src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Http.Extensions.Tests; @@ -19,11 +20,15 @@ public async Task WriteAsync_Skip_NextWriters_WhenResponseAlreadyStarted() new MetadataBasedWriter("FirstWriter"), }); + var services = new ServiceCollection(); + services.AddDefaultHttpJsonOptions(); + var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json" }); var stream = new MemoryStream(); var context = new DefaultHttpContext() { Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, + RequestServices = services.BuildServiceProvider(), }; // Act diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 00ece3149df0..43bb1e1e1573 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -3129,7 +3129,6 @@ public async Task RequestDelegateWritesMembersFromChildTypesToJsonResponseBody_W var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) - .AddSingleton(Options.Create(new JsonOptions())) .BuildServiceProvider(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -3156,7 +3155,6 @@ public async Task RequestDelegateWritesMembersFromChildTypesToJsonResponseBody_W var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) - .AddSingleton(Options.Create(new JsonOptions())) .BuildServiceProvider(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -3183,7 +3181,6 @@ public async Task RequestDelegateWritesJsonTypeDiscriminatorToJsonResponseBody_W var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) - .AddSingleton(Options.Create(new JsonOptions())) .BuildServiceProvider(); var responseBodyStream = new MemoryStream(); @@ -3221,7 +3218,7 @@ public async Task RequestDelegateWritesAsJsonResponseBody_WithJsonSerializerCont var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) - .ConfigureHttpJsonOptions(o => o.SerializerOptions.TypeInfoResolver = TestJsonContext.Default) + .PostConfigure(o => o.SerializerOptions.TypeInfoResolver = TestJsonContext.Default) .BuildServiceProvider(); var responseBodyStream = new MemoryStream(); @@ -3247,7 +3244,7 @@ public void CreateDelegateThrows_WhenGetJsonTypeInfoFail() var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) - .ConfigureHttpJsonOptions(o => o.SerializerOptions.TypeInfoResolver = TestJsonContext.Default) + .PostConfigure(o => o.SerializerOptions.TypeInfoResolver = TestJsonContext.Default) .BuildServiceProvider(); var responseBodyStream = new MemoryStream(); @@ -6883,6 +6880,7 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplemen { new CustomEndpointMetadata { Source = MetadataSource.Caller } }), + ServiceProvider = new ServiceCollection().AddDefaultHttpJsonOptions().BuildServiceProvider(), }; // Act @@ -6904,6 +6902,7 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplemen { new CustomEndpointMetadata { Source = MetadataSource.Caller } }), + ServiceProvider = new ServiceCollection().AddDefaultHttpJsonOptions().BuildServiceProvider(), }; // Act @@ -6925,6 +6924,7 @@ public void Create_CombinesPropertiesAsParameterMetadata_AndTopLevelParameter() { new CustomEndpointMetadata { Source = MetadataSource.Caller } }), + ServiceProvider = new ServiceCollection().AddDefaultHttpJsonOptions().BuildServiceProvider(), }; // Act @@ -6948,6 +6948,7 @@ public void Create_CombinesAllMetadata_InCorrectOrder() { new CustomEndpointMetadata { Source = MetadataSource.Caller } }), + ServiceProvider = new ServiceCollection().AddDefaultHttpJsonOptions().BuildServiceProvider(), }; // Act @@ -7425,7 +7426,7 @@ private DefaultHttpContext CreateHttpContext() return new() { - RequestServices = new ServiceCollection().AddSingleton(LoggerFactory).BuildServiceProvider(), + RequestServices = new ServiceCollection().AddSingleton(LoggerFactory).AddDefaultHttpJsonOptions().BuildServiceProvider(), Features = { [typeof(IHttpResponseFeature)] = responseFeature, diff --git a/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs index b13b868557a1..6c912748bca5 100644 --- a/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs +++ b/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs @@ -98,9 +98,6 @@ public static IServiceCollection AddRouting(this IServiceCollection services) // Set RouteHandlerOptions.ThrowOnBadRequest in development services.TryAddEnumerable(ServiceDescriptor.Transient, ConfigureRouteHandlerOptions>()); - // JsonOptions - services.ConfigureDefaultHttpJsonOptions(); - return services; } diff --git a/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs b/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs index 74e5568ffd9d..b03a1342ed6e 100644 --- a/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs +++ b/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs @@ -37,6 +37,7 @@ public async Task MapPost_FromBodyWorksWithJsonPayload() .ConfigureServices(services => { services.AddRouting(); + services.AddDefaultHttpJsonOptions(); }) .Build(); diff --git a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs index c4924422a748..8c93f8a57b59 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Builder; @@ -518,6 +519,14 @@ public void MapEndpoint_Filter() Assert.Equal("/", endpointBuilder1.RoutePattern.RawText); } + private static DefaultHttpContext CreateHttpContext() + { + var services = new ServiceCollection(); + services.AddDefaultHttpJsonOptions(); + + return new() { RequestServices = services.BuildServiceProvider() }; + } + [Attribute1] [Attribute2] private static Task Handle(HttpContext context) => Task.CompletedTask; From 45b2282f7627a673ab63d92fd12a8ac8d72a3d49 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 30 Jan 2023 22:29:33 -0800 Subject: [PATCH 12/48] Fixing unit tests --- ...lidationProblemDetailsJsonConverterTest.cs | 6 ++- .../test/ProblemDetailsJsonConverterTest.cs | 6 ++- .../src/HttpJsonServiceExtensions.cs | 5 +- .../src/HttpRequestJsonExtensions.cs | 6 ++- .../src/HttpResponseJsonExtensions.cs | 6 ++- src/Http/Http.Extensions/src/JsonOptions.cs | 13 +++++ .../src/RequestDelegateFactory.cs | 3 +- .../test/HttpRequestJsonExtensionsTests.cs | 36 ++++++------- .../test/HttpResponseJsonExtensionsTests.cs | 50 ++++++++----------- .../test/RequestDelegateFactoryTests.cs | 14 ++++-- 10 files changed, 81 insertions(+), 64 deletions(-) diff --git a/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs b/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs index 838bcd836243..a3f2afc9098b 100644 --- a/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs +++ b/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs @@ -3,13 +3,17 @@ using System.Text; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Http.Json; namespace Microsoft.AspNetCore.Http.Abstractions.Tests; public class HttpValidationProblemDetailsJsonConverterTest { - private static JsonSerializerOptions JsonSerializerOptions => new JsonOptions().SerializerOptions; + private static JsonSerializerOptions JsonSerializerOptions => new JsonSerializerOptions(new JsonOptions().SerializerOptions) + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver() + }; [Fact] public void Write_Works() diff --git a/src/Http/Http.Abstractions/test/ProblemDetailsJsonConverterTest.cs b/src/Http/Http.Abstractions/test/ProblemDetailsJsonConverterTest.cs index 666303b71df9..adbc3b49d0b4 100644 --- a/src/Http/Http.Abstractions/test/ProblemDetailsJsonConverterTest.cs +++ b/src/Http/Http.Abstractions/test/ProblemDetailsJsonConverterTest.cs @@ -4,6 +4,7 @@ using System.Text; using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Mvc; @@ -11,7 +12,10 @@ namespace Microsoft.AspNetCore.Http.Abstractions.Tests; public class ProblemDetailsJsonConverterTest { - private static JsonSerializerOptions JsonSerializerOptions => new JsonOptions().SerializerOptions; + private static JsonSerializerOptions JsonSerializerOptions => new JsonSerializerOptions(new JsonOptions().SerializerOptions) + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver() + }; [Fact] public void Read_ThrowsIfJsonIsIncomplete() diff --git a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs index 4c7d25ab3c6d..4d1b90dd4efa 100644 --- a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs @@ -26,9 +26,8 @@ public static class HttpJsonServiceExtensions /// The modified . public static IServiceCollection ConfigureHttpJsonOptions(this IServiceCollection services, Action configureOptions) { - services.Configure(configureOptions); - - return services.AddDefaultHttpJsonOptions(); + services.AddDefaultHttpJsonOptions(); + return services.Configure(configureOptions); } /// diff --git a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs index 437283c330dd..a22f61fec852 100644 --- a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs @@ -7,6 +7,7 @@ using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Http.Json; +using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -303,7 +304,10 @@ private static bool HasJsonContentType(this HttpRequest request, out StringSegme private static JsonSerializerOptions ResolveSerializerOptions(HttpContext httpContext) { // Attempt to resolve options from DI then fallback to default options - return httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; + var options = httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions; + options ??= TrimmingAppContextSwitches.EnsureJsonTrimmability ? JsonOptions.DefaultSerializerOptions : JsonOptions.ReflectionBasedSerializerOptions; + + return options; } [DoesNotReturn] diff --git a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs index d2ec3b1e516e..7576a3f9948d 100644 --- a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs @@ -6,6 +6,7 @@ using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Http.Json; +using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -339,6 +340,9 @@ async Task WriteAsJsonAsyncSlow() private static JsonSerializerOptions ResolveSerializerOptions(HttpContext httpContext) { // Attempt to resolve options from DI then fallback to default options - return httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; + var options = httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions; + options ??= TrimmingAppContextSwitches.EnsureJsonTrimmability ? JsonOptions.DefaultSerializerOptions : JsonOptions.ReflectionBasedSerializerOptions; + + return options; } } diff --git a/src/Http/Http.Extensions/src/JsonOptions.cs b/src/Http/Http.Extensions/src/JsonOptions.cs index 4b4296d38584..3a392dc733f6 100644 --- a/src/Http/Http.Extensions/src/JsonOptions.cs +++ b/src/Http/Http.Extensions/src/JsonOptions.cs @@ -1,8 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Text.Encodings.Web; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; #nullable enable @@ -14,6 +16,7 @@ namespace Microsoft.AspNetCore.Http.Json; /// public class JsonOptions { + private static JsonSerializerOptions? _reflectionSerializerOptions; internal static readonly JsonSerializerOptions DefaultSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) { // Web defaults don't use the relex JSON escaping encoder. @@ -23,6 +26,16 @@ public class JsonOptions Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, }; + internal static JsonSerializerOptions ReflectionBasedSerializerOptions + { + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use the 'DefaultSerializerOptions' property instead.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the 'DefaultSerializerOptions' property instead.")] + get => _reflectionSerializerOptions ??= new JsonSerializerOptions(DefaultSerializerOptions) + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver() + }; + } + // Use a copy so the defaults are not modified. /// /// Gets the . diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 409daf83f94a..b2ff3a77c6d7 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -262,7 +262,8 @@ private static RequestDelegateFactoryContext CreateFactoryContext(RequestDelegat var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices ?? EmptyServiceProvider.Instance; var endpointBuilder = options?.EndpointBuilder ?? new RdfEndpointBuilder(serviceProvider); - var jsonSerializerOptions = serviceProvider.GetService>()?.Value.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; + var jsonSerializerOptions = serviceProvider.GetService>()?.Value.SerializerOptions; + jsonSerializerOptions ??= TrimmingAppContextSwitches.EnsureJsonTrimmability ? JsonOptions.DefaultSerializerOptions : JsonOptions.ReflectionBasedSerializerOptions; var factoryContext = new RequestDelegateFactoryContext { diff --git a/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs b/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs index 8c2031357cb9..e761e95ee70b 100644 --- a/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs +++ b/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs @@ -36,7 +36,7 @@ public void HasJsonContentType(string contentType, bool hasJsonContentType) public async Task ReadFromJsonAsyncGeneric_NonJsonContentType_ThrowError() { // Arrange - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Request.ContentType = "text/json"; // Act @@ -51,7 +51,7 @@ public async Task ReadFromJsonAsyncGeneric_NonJsonContentType_ThrowError() public async Task ReadFromJsonAsyncGeneric_NoBodyContent_ThrowError() { // Arrange - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Request.ContentType = "application/json"; // Act @@ -66,7 +66,7 @@ public async Task ReadFromJsonAsyncGeneric_NoBodyContent_ThrowError() public async Task ReadFromJsonAsyncGeneric_ValidBodyContent_ReturnValue() { // Arrange - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Request.ContentType = "application/json"; context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("1")); @@ -81,7 +81,7 @@ public async Task ReadFromJsonAsyncGeneric_ValidBodyContent_ReturnValue() public async Task ReadFromJsonAsyncGeneric_WithOptions_ReturnValue() { // Arrange - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Request.ContentType = "application/json"; context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("[1,2,]")); @@ -102,7 +102,7 @@ public async Task ReadFromJsonAsyncGeneric_WithOptions_ReturnValue() public async Task ReadFromJsonAsyncGeneric_Utf8Encoding_ReturnValue() { // Arrange - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Request.ContentType = "application/json; charset=utf-8"; context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("[1,2]")); @@ -120,7 +120,7 @@ public async Task ReadFromJsonAsyncGeneric_Utf8Encoding_ReturnValue() public async Task ReadFromJsonAsyncGeneric_Utf16Encoding_ReturnValue() { // Arrange - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Request.ContentType = "application/json; charset=utf-16"; context.Request.Body = new MemoryStream(Encoding.Unicode.GetBytes(@"{""name"": ""激光這兩個字是甚麼意思""}")); @@ -135,7 +135,7 @@ public async Task ReadFromJsonAsyncGeneric_Utf16Encoding_ReturnValue() public async Task ReadFromJsonAsyncGeneric_WithCancellationToken_CancellationRaised() { // Arrange - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Request.ContentType = "application /json"; context.Request.Body = new TestStream(); @@ -155,7 +155,7 @@ public async Task ReadFromJsonAsyncGeneric_WithCancellationToken_CancellationRai public async Task ReadFromJsonAsyncGeneric_InvalidEncoding_ThrowError() { // Arrange - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Request.ContentType = "application/json; charset=invalid"; // Act @@ -169,7 +169,7 @@ public async Task ReadFromJsonAsyncGeneric_InvalidEncoding_ThrowError() public async Task ReadFromJsonAsync_ValidBodyContent_ReturnValue() { // Arrange - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Request.ContentType = "application/json"; context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("1")); @@ -184,7 +184,7 @@ public async Task ReadFromJsonAsync_ValidBodyContent_ReturnValue() public async Task ReadFromJsonAsync_Utf16Encoding_ReturnValue() { // Arrange - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Request.ContentType = "application/json; charset=utf-16"; context.Request.Body = new MemoryStream(Encoding.Unicode.GetBytes(@"{""name"": ""激光這兩個字是甚麼意思""}")); @@ -199,7 +199,7 @@ public async Task ReadFromJsonAsync_Utf16Encoding_ReturnValue() public async Task ReadFromJsonAsync_InvalidEncoding_ThrowError() { // Arrange - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Request.ContentType = "application/json; charset=invalid"; // Act @@ -213,7 +213,7 @@ public async Task ReadFromJsonAsync_InvalidEncoding_ThrowError() public async Task ReadFromJsonAsync_WithOptions_ReturnValue() { // Arrange - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Request.ContentType = "application/json"; context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("[1,2,]")); @@ -234,7 +234,7 @@ public async Task ReadFromJsonAsync_WithOptions_ReturnValue() public async Task ReadFromJsonAsync_WithTypeInfo_ReturnValue() { // Arrange - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Request.ContentType = "application/json"; context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("[1,2,]")); @@ -256,7 +256,7 @@ public async Task ReadFromJsonAsync_WithTypeInfo_ReturnValue() public async Task ReadFromJsonAsync_WithGenericTypeInfo_ReturnValue() { // Arrange - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Request.ContentType = "application/json"; context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("[1,2,]")); @@ -274,12 +274,4 @@ public async Task ReadFromJsonAsync_WithGenericTypeInfo_ReturnValue() i => Assert.Equal(1, i), i => Assert.Equal(2, i)); } - - private DefaultHttpContext CreateHttpContext() - { - var services = new ServiceCollection(); - services.AddDefaultHttpJsonOptions(); - - return new() { RequestServices = services.BuildServiceProvider() }; - } } diff --git a/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs b/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs index 8baa62e1e0f1..58059b9ec9e0 100644 --- a/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs +++ b/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs @@ -20,7 +20,7 @@ public async Task WriteAsJsonAsyncGeneric_SimpleValue_JsonResponse() { // Arrange var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; // Act @@ -39,7 +39,7 @@ public async Task WriteAsJsonAsyncGeneric_NullValue_JsonResponse() { // Arrange var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; // Act @@ -57,7 +57,7 @@ public async Task WriteAsJsonAsyncGeneric_WithOptions_JsonResponse() { // Arrange var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; // Act @@ -90,7 +90,7 @@ public async Task WriteAsJsonAsyncGeneric_CustomStatusCode_StatusCodeUnchanged() { // Arrange var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; // Act @@ -107,7 +107,7 @@ public async Task WriteAsJsonAsyncGeneric_WithContentType_JsonResponseWithCustom { // Arrange var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; // Act @@ -121,7 +121,7 @@ public async Task WriteAsJsonAsyncGeneric_WithContentType_JsonResponseWithCustom public async Task WriteAsJsonAsyncGeneric_WithCancellationToken_CancellationRaised() { // Arrange - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = new TestStream(); var cts = new CancellationTokenSource(); @@ -141,7 +141,7 @@ public async Task WriteAsJsonAsyncGeneric_ObjectWithStrings_CamcelCaseAndNotEsca { // Arrange var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; var value = new TestObject { @@ -161,7 +161,7 @@ public async Task WriteAsJsonAsync_SimpleValue_JsonResponse() { // Arrange var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; // Act @@ -180,7 +180,7 @@ public async Task WriteAsJsonAsync_NullValue_JsonResponse() { // Arrange var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; // Act @@ -198,7 +198,7 @@ public async Task WriteAsJsonAsync_NullType_ThrowsArgumentNullException() { // Arrange var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; // Act & Assert @@ -210,7 +210,7 @@ public async Task WriteAsJsonAsync_NullResponse_ThrowsArgumentNullException() { // Arrange var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; // Act & Assert @@ -222,7 +222,7 @@ public async Task WriteAsJsonAsync_ObjectWithStrings_CamcelCaseAndNotEscaped() { // Arrange var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; var value = new TestObject { @@ -242,7 +242,7 @@ public async Task WriteAsJsonAsync_CustomStatusCode_StatusCodeUnchanged() { // Arrange var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; // Act @@ -259,7 +259,7 @@ public async Task WriteAsJsonAsyncGeneric_AsyncEnumerable() { // Arrange var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; // Act @@ -284,7 +284,7 @@ public async Task WriteAsJsonAsync_AsyncEnumerable() { // Arrange var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; // Act @@ -310,7 +310,7 @@ public async Task WriteAsJsonAsyncGeneric_AsyncEnumerable_ClosedConnecton() // Arrange var cts = new CancellationTokenSource(); var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; context.RequestAborted = cts.Token; var iterated = false; @@ -344,7 +344,7 @@ public async Task WriteAsJsonAsync_AsyncEnumerable_ClosedConnecton() // Arrange var cts = new CancellationTokenSource(); var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; context.RequestAborted = cts.Token; var iterated = false; @@ -377,7 +377,7 @@ public async Task WriteAsJsonAsync_AsyncEnumerable_UserPassedTokenThrows() { // Arrange var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; context.RequestAborted = new CancellationToken(canceled: true); var cts = new CancellationTokenSource(); @@ -411,7 +411,7 @@ public async Task WriteAsJsonAsyncGeneric_AsyncEnumerable_UserPassedTokenThrows( { // Arrange var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; context.RequestAborted = new CancellationToken(canceled: true); var cts = new CancellationTokenSource(); @@ -445,7 +445,7 @@ public async Task WriteAsJsonAsyncGeneric_WithJsonTypeInfo_JsonResponse() { // Arrange var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; // Act @@ -466,7 +466,7 @@ public async Task WriteAsJsonAsync_NullValue_WithJsonTypeInfo_JsonResponse() { // Arrange var body = new MemoryStream(); - var context = CreateHttpContext(); + var context = new DefaultHttpContext(); context.Response.Body = body; // Act @@ -482,14 +482,6 @@ public async Task WriteAsJsonAsync_NullValue_WithJsonTypeInfo_JsonResponse() Assert.Equal("null", data); } - private static DefaultHttpContext CreateHttpContext() - { - var services = new ServiceCollection(); - services.AddDefaultHttpJsonOptions(); - - return new() { RequestServices = services.BuildServiceProvider() }; - } - public class TestObject { public string? StringProperty { get; set; } diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 43bb1e1e1573..24787df9c04a 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -1642,6 +1642,7 @@ public async Task BindAsyncRunsBeforeBodyBinding() httpContext.Features.Set(new RequestBodyDetectionFeature(true)); var jsonOptions = new JsonOptions(); + jsonOptions.SerializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); jsonOptions.SerializerOptions.Converters.Add(new TodoJsonConverter()); var mock = new Mock(); @@ -1880,6 +1881,7 @@ public async Task RequestDelegatePopulatesFromBodyParameter(Delegate action) httpContext.Features.Set(new RequestBodyDetectionFeature(true)); var jsonOptions = new JsonOptions(); + jsonOptions.SerializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); jsonOptions.SerializerOptions.Converters.Add(new TodoJsonConverter()); var mock = new Mock(); @@ -3218,6 +3220,7 @@ public async Task RequestDelegateWritesAsJsonResponseBody_WithJsonSerializerCont var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) + .AddDefaultHttpJsonOptions() .PostConfigure(o => o.SerializerOptions.TypeInfoResolver = TestJsonContext.Default) .BuildServiceProvider(); @@ -3244,6 +3247,7 @@ public void CreateDelegateThrows_WhenGetJsonTypeInfoFail() var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) + .AddDefaultHttpJsonOptions() .PostConfigure(o => o.SerializerOptions.TypeInfoResolver = TestJsonContext.Default) .BuildServiceProvider(); @@ -6880,7 +6884,7 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplemen { new CustomEndpointMetadata { Source = MetadataSource.Caller } }), - ServiceProvider = new ServiceCollection().AddDefaultHttpJsonOptions().BuildServiceProvider(), + ServiceProvider = new ServiceCollection().BuildServiceProvider(), }; // Act @@ -6902,7 +6906,7 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplemen { new CustomEndpointMetadata { Source = MetadataSource.Caller } }), - ServiceProvider = new ServiceCollection().AddDefaultHttpJsonOptions().BuildServiceProvider(), + ServiceProvider = new ServiceCollection().BuildServiceProvider(), }; // Act @@ -6924,7 +6928,7 @@ public void Create_CombinesPropertiesAsParameterMetadata_AndTopLevelParameter() { new CustomEndpointMetadata { Source = MetadataSource.Caller } }), - ServiceProvider = new ServiceCollection().AddDefaultHttpJsonOptions().BuildServiceProvider(), + ServiceProvider = new ServiceCollection().BuildServiceProvider(), }; // Act @@ -6948,7 +6952,7 @@ public void Create_CombinesAllMetadata_InCorrectOrder() { new CustomEndpointMetadata { Source = MetadataSource.Caller } }), - ServiceProvider = new ServiceCollection().AddDefaultHttpJsonOptions().BuildServiceProvider(), + ServiceProvider = new ServiceCollection().BuildServiceProvider(), }; // Act @@ -7426,7 +7430,7 @@ private DefaultHttpContext CreateHttpContext() return new() { - RequestServices = new ServiceCollection().AddSingleton(LoggerFactory).AddDefaultHttpJsonOptions().BuildServiceProvider(), + RequestServices = new ServiceCollection().AddSingleton(LoggerFactory).BuildServiceProvider(), Features = { [typeof(IHttpResponseFeature)] = responseFeature, From bb507c792876cbe2391b214b7dca46de125347f3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 31 Jan 2023 13:15:57 -0800 Subject: [PATCH 13/48] Trying a different approach --- src/DefaultBuilder/src/WebHost.cs | 4 --- .../src/DefaultHttpJsonOptionsSetup.cs | 24 --------------- .../src/HttpJsonServiceExtensions.cs | 24 --------------- .../src/HttpRequestJsonExtensions.cs | 9 ++---- .../src/HttpResponseJsonExtensions.cs | 9 ++---- src/Http/Http.Extensions/src/JsonOptions.cs | 13 -------- .../src/PublicAPI.Unshipped.txt | 1 - .../src/RequestDelegateFactory.cs | 3 +- .../test/ProblemDetailsServiceTest.cs | 1 - .../test/RequestDelegateFactoryTests.cs | 6 ++-- .../src/Microsoft.AspNetCore.Routing.csproj | 1 + .../test/FunctionalTests/RouteHandlerTest.cs | 1 - ...egateEndpointRouteBuilderExtensionsTest.cs | 8 ----- src/Shared/Json/JsonSerializerExtensions.cs | 30 ++++++++++++++++++- 14 files changed, 39 insertions(+), 95 deletions(-) delete mode 100644 src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs diff --git a/src/DefaultBuilder/src/WebHost.cs b/src/DefaultBuilder/src/WebHost.cs index 5563b69c31ef..4a49c3ef8e3c 100644 --- a/src/DefaultBuilder/src/WebHost.cs +++ b/src/DefaultBuilder/src/WebHost.cs @@ -258,10 +258,6 @@ internal static void UseKestrel(IWebHostBuilder builder) services.AddTransient, ForwardedHeadersOptionsSetup>(); services.AddRouting(); - - // JsonOptions - services.AddDefaultHttpJsonOptions(); - }); } diff --git a/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs b/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs deleted file mode 100644 index 3046570eb759..000000000000 --- a/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization.Metadata; -using Microsoft.AspNetCore.Http.Json; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Http; - -[RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] -[RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] -internal sealed class DefaultHttpJsonOptionsSetup : IPostConfigureOptions -{ - public void PostConfigure(string? name, JsonOptions options) - { - InitializeForReflection(options); - } - - private static void InitializeForReflection(JsonOptions options) - { - options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(options.SerializerOptions.TypeInfoResolver, new DefaultJsonTypeInfoResolver()); - } -} diff --git a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs index 4d1b90dd4efa..408df5a4a5da 100644 --- a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs @@ -26,30 +26,6 @@ public static class HttpJsonServiceExtensions /// The modified . public static IServiceCollection ConfigureHttpJsonOptions(this IServiceCollection services, Action configureOptions) { - services.AddDefaultHttpJsonOptions(); return services.Configure(configureOptions); } - - /// - /// - /// - /// - /// - public static IServiceCollection AddDefaultHttpJsonOptions(this IServiceCollection services) - { - ArgumentNullException.ThrowIfNull(services); - - services.AddOptions(); - - if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) - { -#pragma warning disable IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml -#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. - services.TryAddEnumerable(ServiceDescriptor.Singleton, DefaultHttpJsonOptionsSetup>()); -#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. -#pragma warning restore IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml - } - - return services; - } } diff --git a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs index a22f61fec852..5d4423c88d77 100644 --- a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs @@ -40,7 +40,7 @@ public static class HttpRequestJsonExtensions ArgumentNullException.ThrowIfNull(request); var options = ResolveSerializerOptions(request.HttpContext); - return request.ReadFromJsonAsync(jsonTypeInfo: (JsonTypeInfo)options.GetTypeInfo(typeof(TValue)), cancellationToken); + return request.ReadFromJsonAsync(jsonTypeInfo: (JsonTypeInfo)options.GetReadOnlyTypeInfo(typeof(TValue)), cancellationToken); } /// @@ -176,7 +176,7 @@ public static class HttpRequestJsonExtensions ArgumentNullException.ThrowIfNull(request); var options = ResolveSerializerOptions(request.HttpContext); - return request.ReadFromJsonAsync(jsonTypeInfo: options.GetTypeInfo(type), cancellationToken); + return request.ReadFromJsonAsync(jsonTypeInfo: options.GetReadOnlyTypeInfo(type), cancellationToken); } /// @@ -304,10 +304,7 @@ private static bool HasJsonContentType(this HttpRequest request, out StringSegme private static JsonSerializerOptions ResolveSerializerOptions(HttpContext httpContext) { // Attempt to resolve options from DI then fallback to default options - var options = httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions; - options ??= TrimmingAppContextSwitches.EnsureJsonTrimmability ? JsonOptions.DefaultSerializerOptions : JsonOptions.ReflectionBasedSerializerOptions; - - return options; + return httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; } [DoesNotReturn] diff --git a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs index 7576a3f9948d..6e334d29a591 100644 --- a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs @@ -39,7 +39,7 @@ public static Task WriteAsJsonAsync( ArgumentNullException.ThrowIfNull(response); var options = ResolveSerializerOptions(response.HttpContext); - return response.WriteAsJsonAsync(value, jsonTypeInfo: (JsonTypeInfo)options.GetTypeInfo(typeof(TValue)), contentType: null, cancellationToken); + return response.WriteAsJsonAsync(value, jsonTypeInfo: (JsonTypeInfo)options.GetReadOnlyTypeInfo(typeof(TValue)), contentType: null, cancellationToken); } /// @@ -214,7 +214,7 @@ public static Task WriteAsJsonAsync( ArgumentNullException.ThrowIfNull(response); var options = ResolveSerializerOptions(response.HttpContext); - return response.WriteAsJsonAsync(value, jsonTypeInfo: options.GetTypeInfo(type), contentType: null, cancellationToken); + return response.WriteAsJsonAsync(value, jsonTypeInfo: options.GetReadOnlyTypeInfo(type), contentType: null, cancellationToken); } /// @@ -340,9 +340,6 @@ async Task WriteAsJsonAsyncSlow() private static JsonSerializerOptions ResolveSerializerOptions(HttpContext httpContext) { // Attempt to resolve options from DI then fallback to default options - var options = httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions; - options ??= TrimmingAppContextSwitches.EnsureJsonTrimmability ? JsonOptions.DefaultSerializerOptions : JsonOptions.ReflectionBasedSerializerOptions; - - return options; + return httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; } } diff --git a/src/Http/Http.Extensions/src/JsonOptions.cs b/src/Http/Http.Extensions/src/JsonOptions.cs index 3a392dc733f6..4b4296d38584 100644 --- a/src/Http/Http.Extensions/src/JsonOptions.cs +++ b/src/Http/Http.Extensions/src/JsonOptions.cs @@ -1,10 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; using System.Text.Encodings.Web; using System.Text.Json; -using System.Text.Json.Serialization.Metadata; #nullable enable @@ -16,7 +14,6 @@ namespace Microsoft.AspNetCore.Http.Json; /// public class JsonOptions { - private static JsonSerializerOptions? _reflectionSerializerOptions; internal static readonly JsonSerializerOptions DefaultSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) { // Web defaults don't use the relex JSON escaping encoder. @@ -26,16 +23,6 @@ public class JsonOptions Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, }; - internal static JsonSerializerOptions ReflectionBasedSerializerOptions - { - [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use the 'DefaultSerializerOptions' property instead.")] - [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the 'DefaultSerializerOptions' property instead.")] - get => _reflectionSerializerOptions ??= new JsonSerializerOptions(DefaultSerializerOptions) - { - TypeInfoResolver = new DefaultJsonTypeInfoResolver() - }; - } - // Use a copy so the defaults are not modified. /// /// Gets the . diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index 1a9348da333d..254e8dfb2161 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,4 +1,3 @@ #nullable enable static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, object? value, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, string? contentType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.AddDefaultHttpJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index b2ff3a77c6d7..409daf83f94a 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -262,8 +262,7 @@ private static RequestDelegateFactoryContext CreateFactoryContext(RequestDelegat var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices ?? EmptyServiceProvider.Instance; var endpointBuilder = options?.EndpointBuilder ?? new RdfEndpointBuilder(serviceProvider); - var jsonSerializerOptions = serviceProvider.GetService>()?.Value.SerializerOptions; - jsonSerializerOptions ??= TrimmingAppContextSwitches.EnsureJsonTrimmability ? JsonOptions.DefaultSerializerOptions : JsonOptions.ReflectionBasedSerializerOptions; + var jsonSerializerOptions = serviceProvider.GetService>()?.Value.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; var factoryContext = new RequestDelegateFactoryContext { diff --git a/src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs index 7cbfbbba6e98..38245ebeb087 100644 --- a/src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs @@ -21,7 +21,6 @@ public async Task WriteAsync_Skip_NextWriters_WhenResponseAlreadyStarted() }); var services = new ServiceCollection(); - services.AddDefaultHttpJsonOptions(); var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json" }); var stream = new MemoryStream(); diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 24787df9c04a..b6ae6834f7c7 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -3220,8 +3220,7 @@ public async Task RequestDelegateWritesAsJsonResponseBody_WithJsonSerializerCont var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) - .AddDefaultHttpJsonOptions() - .PostConfigure(o => o.SerializerOptions.TypeInfoResolver = TestJsonContext.Default) + .ConfigureHttpJsonOptions(o => o.SerializerOptions.TypeInfoResolver = TestJsonContext.Default) .BuildServiceProvider(); var responseBodyStream = new MemoryStream(); @@ -3247,8 +3246,7 @@ public void CreateDelegateThrows_WhenGetJsonTypeInfoFail() var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) - .AddDefaultHttpJsonOptions() - .PostConfigure(o => o.SerializerOptions.TypeInfoResolver = TestJsonContext.Default) + .ConfigureHttpJsonOptions(o => o.SerializerOptions.TypeInfoResolver = TestJsonContext.Default) .BuildServiceProvider(); var responseBodyStream = new MemoryStream(); diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index bfa84d35edb5..228908dd25d2 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -33,6 +33,7 @@ + diff --git a/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs b/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs index b03a1342ed6e..74e5568ffd9d 100644 --- a/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs +++ b/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs @@ -37,7 +37,6 @@ public async Task MapPost_FromBodyWorksWithJsonPayload() .ConfigureServices(services => { services.AddRouting(); - services.AddDefaultHttpJsonOptions(); }) .Build(); diff --git a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs index 8c93f8a57b59..6a6880273dd4 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs @@ -519,14 +519,6 @@ public void MapEndpoint_Filter() Assert.Equal("/", endpointBuilder1.RoutePattern.RawText); } - private static DefaultHttpContext CreateHttpContext() - { - var services = new ServiceCollection(); - services.AddDefaultHttpJsonOptions(); - - return new() { RequestServices = services.BuildServiceProvider() }; - } - [Attribute1] [Attribute2] private static Task Handle(HttpContext context) => Task.CompletedTask; diff --git a/src/Shared/Json/JsonSerializerExtensions.cs b/src/Shared/Json/JsonSerializerExtensions.cs index bf53e0f21993..906a644fe330 100644 --- a/src/Shared/Json/JsonSerializerExtensions.cs +++ b/src/Shared/Json/JsonSerializerExtensions.cs @@ -1,8 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using Microsoft.AspNetCore.Internal; namespace Microsoft.AspNetCore.Http; @@ -13,7 +15,33 @@ public static bool IsPolymorphicSafe(this JsonTypeInfo jsonTypeInfo) public static JsonTypeInfo GetReadOnlyTypeInfo(this JsonSerializerOptions options, Type type) { - options.MakeReadOnly(); + options.Configure(); + return options.GetTypeInfo(type); } + + public static void Configure(this JsonSerializerOptions options) + { + if (!options.IsReadOnly) + { + if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) + { +#pragma warning disable IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml +#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. + InitializeForReflection(options); +#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. +#pragma warning restore IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml + } + + options.MakeReadOnly(); + } + } + + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] + private static void InitializeForReflection(JsonSerializerOptions options) + { + // TODO: Should we combine or set only when null? + options.TypeInfoResolver ??= new DefaultJsonTypeInfoResolver(); + } } From f52c53556582afb2c6d3cb4bbeca1c635d445d02 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 31 Jan 2023 14:28:04 -0800 Subject: [PATCH 14/48] Changing MVC --- .../src/HttpJsonServiceExtensions.cs | 4 ---- .../src/HttpRequestJsonExtensions.cs | 1 - .../src/HttpResponseJsonExtensions.cs | 1 - ...re.Http.Extensions.WarningSuppressions.xml | 6 ++--- ...AspNetCore.Routing.WarningSuppressions.xml | 12 ++++++++++ .../MvcCoreJsonOptionsSetup.cs | 24 ------------------- .../MvcCoreServiceCollectionExtensions.cs | 6 ----- .../SystemTextJsonOutputFormatter.cs | 3 +-- 8 files changed, 16 insertions(+), 41 deletions(-) create mode 100644 src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml delete mode 100644 src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreJsonOptionsSetup.cs diff --git a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs index 408df5a4a5da..5e3ca99aac35 100644 --- a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs @@ -1,11 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Json; -using Microsoft.AspNetCore.Internal; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection; diff --git a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs index 5d4423c88d77..8d0a08a7be4e 100644 --- a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs @@ -7,7 +7,6 @@ using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Http.Json; -using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; diff --git a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs index 6e334d29a591..e6903d8725f2 100644 --- a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs @@ -6,7 +6,6 @@ using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Http.Json; -using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml index db9409dec8da..a807f7ba044c 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml @@ -1,12 +1,12 @@ - + ILLink IL2026 member - M:Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.AddDefaultHttpJsonOptions(Microsoft.Extensions.DependencyInjection.IServiceCollection) + M:Microsoft.AspNetCore.Http.JsonSerializerExtensions.Configure(System.Text.Json.JsonSerializerOptions) This warning is left in the product so developers get an ILLink warning when trimming an app only when Microsoft.AspNetCore.EnsureJsonTrimmability=false. - + \ No newline at end of file diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml new file mode 100644 index 000000000000..6a75f00c1603 --- /dev/null +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml @@ -0,0 +1,12 @@ + + + + + ILLink + IL2026 + member + M:Microsoft.AspNetCore.Http.JsonSerializerExtensions.Configure(System.Text.Json.JsonSerializerOptions) + This warning is left in the product so developers get an ILLink warning when trimming an app only when Microsoft.AspNetCore.EnsureJsonTrimmability=false. + + + \ No newline at end of file diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreJsonOptionsSetup.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreJsonOptionsSetup.cs deleted file mode 100644 index 32cc77cf19c9..000000000000 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreJsonOptionsSetup.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization.Metadata; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.DependencyInjection; - -[RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] -[RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] -internal sealed class MvcCoreJsonOptionsSetup : IPostConfigureOptions -{ - public void PostConfigure(string? name, JsonOptions options) - { - InitializeForReflection(options); - } - - private static void InitializeForReflection(JsonOptions options) - { - options.JsonSerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(options.JsonSerializerOptions.TypeInfoResolver, new DefaultJsonTypeInfoResolver()); - } -} diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 3709890be47b..f2f5643224c3 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -141,12 +141,6 @@ internal static void AddMvcCoreServices(IServiceCollection services) services.TryAddEnumerable( ServiceDescriptor.Transient, MvcCoreRouteOptionsSetup>()); - if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) - { - services.TryAddEnumerable( - ServiceDescriptor.Transient, MvcCoreJsonOptionsSetup>()); - } - // // Action Discovery // diff --git a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs index fb0833174549..2ac07771fcb5 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs @@ -21,10 +21,9 @@ public class SystemTextJsonOutputFormatter : TextOutputFormatter /// The . public SystemTextJsonOutputFormatter(JsonSerializerOptions jsonSerializerOptions) { + jsonSerializerOptions.Configure(); SerializerOptions = jsonSerializerOptions; - jsonSerializerOptions.MakeReadOnly(); - SupportedEncodings.Add(Encoding.UTF8); SupportedEncodings.Add(Encoding.Unicode); SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationJson); From 4298afc2b6e3ef29124196ad6eaa0bd4e193e437 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Feb 2023 09:53:17 -0800 Subject: [PATCH 15/48] Clean up --- src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs | 3 ++- .../Http.Extensions/test/HttpResponseJsonExtensionsTests.cs | 2 -- src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs | 4 ---- src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs | 4 ---- .../RequestDelegateEndpointRouteBuilderExtensionsTest.cs | 1 - .../DependencyInjection/MvcCoreServiceCollectionExtensions.cs | 1 - src/Shared/Json/JsonSerializerExtensions.cs | 3 +-- 7 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs index 5e3ca99aac35..c5ad6b8071bc 100644 --- a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs @@ -22,6 +22,7 @@ public static class HttpJsonServiceExtensions /// The modified . public static IServiceCollection ConfigureHttpJsonOptions(this IServiceCollection services, Action configureOptions) { - return services.Configure(configureOptions); + services.Configure(configureOptions); + return services; } } diff --git a/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs b/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs index 58059b9ec9e0..8bed397549c2 100644 --- a/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs +++ b/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs @@ -6,8 +6,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; -using Microsoft.AspNetCore.Testing; -using Microsoft.Extensions.DependencyInjection; #nullable enable diff --git a/src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs index 38245ebeb087..66abd0de36a3 100644 --- a/src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; -using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Http.Extensions.Tests; @@ -20,14 +19,11 @@ public async Task WriteAsync_Skip_NextWriters_WhenResponseAlreadyStarted() new MetadataBasedWriter("FirstWriter"), }); - var services = new ServiceCollection(); - var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json" }); var stream = new MemoryStream(); var context = new DefaultHttpContext() { Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest }, - RequestServices = services.BuildServiceProvider(), }; // Act diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index b6ae6834f7c7..564da78c89f0 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -6882,7 +6882,6 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplemen { new CustomEndpointMetadata { Source = MetadataSource.Caller } }), - ServiceProvider = new ServiceCollection().BuildServiceProvider(), }; // Act @@ -6904,7 +6903,6 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplemen { new CustomEndpointMetadata { Source = MetadataSource.Caller } }), - ServiceProvider = new ServiceCollection().BuildServiceProvider(), }; // Act @@ -6926,7 +6924,6 @@ public void Create_CombinesPropertiesAsParameterMetadata_AndTopLevelParameter() { new CustomEndpointMetadata { Source = MetadataSource.Caller } }), - ServiceProvider = new ServiceCollection().BuildServiceProvider(), }; // Act @@ -6950,7 +6947,6 @@ public void Create_CombinesAllMetadata_InCorrectOrder() { new CustomEndpointMetadata { Source = MetadataSource.Caller } }), - ServiceProvider = new ServiceCollection().BuildServiceProvider(), }; // Act diff --git a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs index 6a6880273dd4..c4924422a748 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs @@ -11,7 +11,6 @@ using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Builder; diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index f2f5643224c3..a8178f76ca78 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -5,7 +5,6 @@ using System.Linq; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; diff --git a/src/Shared/Json/JsonSerializerExtensions.cs b/src/Shared/Json/JsonSerializerExtensions.cs index 906a644fe330..f896b3259309 100644 --- a/src/Shared/Json/JsonSerializerExtensions.cs +++ b/src/Shared/Json/JsonSerializerExtensions.cs @@ -41,7 +41,6 @@ public static void Configure(this JsonSerializerOptions options) [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] private static void InitializeForReflection(JsonSerializerOptions options) { - // TODO: Should we combine or set only when null? - options.TypeInfoResolver ??= new DefaultJsonTypeInfoResolver(); + options.TypeInfoResolver = JsonTypeInfoResolver.Combine(options.TypeInfoResolver, new DefaultJsonTypeInfoResolver()); } } From 60f9ade36d8568f614b8ea0af2fd482c2faccb07 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Feb 2023 11:49:52 -0800 Subject: [PATCH 16/48] Clean up & Fix/Add unit tests --- .../Http.Extensions/test/JsonOptionsTests.cs | 47 -------- .../test/JsonSerializerExtensionsTests.cs | 102 ++++++++++++++++++ .../test/RequestDelegateFactoryTests.cs | 44 +++++--- .../src/Microsoft.AspNetCore.Routing.csproj | 4 + .../src/Properties/ILLink.Substitutions.xml | 8 ++ .../SystemTextJsonOutputFormatterTest.cs | 22 +++- ...lidationProblemDetailsJsonConverterTest.cs | 6 +- src/Mvc/Mvc.Core/test/JsonOptionsTest.cs | 46 -------- src/Shared/Json/JsonSerializerExtensions.cs | 7 +- 9 files changed, 176 insertions(+), 110 deletions(-) delete mode 100644 src/Http/Http.Extensions/test/JsonOptionsTests.cs create mode 100644 src/Http/Http.Extensions/test/JsonSerializerExtensionsTests.cs create mode 100644 src/Http/Routing/src/Properties/ILLink.Substitutions.xml delete mode 100644 src/Mvc/Mvc.Core/test/JsonOptionsTest.cs diff --git a/src/Http/Http.Extensions/test/JsonOptionsTests.cs b/src/Http/Http.Extensions/test/JsonOptionsTests.cs deleted file mode 100644 index 2bae1a700e66..000000000000 --- a/src/Http/Http.Extensions/test/JsonOptionsTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Text.Json.Serialization.Metadata; -using Microsoft.AspNetCore.Http.Json; -using Microsoft.AspNetCore.Testing; -using Microsoft.DotNet.RemoteExecutor; - -namespace Microsoft.AspNetCore.Http.Extensions; - -public class JsonOptionsTests -{ - [ConditionalFact] - [RemoteExecutionSupported] - public void DefaultSerializerOptions_SetsTypeInfoResolverNull_WhenEnsureJsonTrimmabilityTrue() - { - var options = new RemoteInvokeOptions(); - options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); - - using var remoteHandle = RemoteExecutor.Invoke(static () => - { - // Arrange - var options = JsonOptions.DefaultSerializerOptions; - - // Assert - Assert.Null(options.TypeInfoResolver); - }, options); - } - - [ConditionalFact] - [RemoteExecutionSupported] - public void DefaultSerializerOptions_SetsTypeInfoResolverToDefault_WhenEnsureJsonTrimmabilityFalse() - { - var options = new RemoteInvokeOptions(); - options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); - - using var remoteHandle = RemoteExecutor.Invoke(static () => - { - // Arrange - var options = JsonOptions.DefaultSerializerOptions; - - // Assert - Assert.NotNull(options.TypeInfoResolver); - Assert.IsType(options.TypeInfoResolver); - }, options); - } -} diff --git a/src/Http/Http.Extensions/test/JsonSerializerExtensionsTests.cs b/src/Http/Http.Extensions/test/JsonSerializerExtensionsTests.cs new file mode 100644 index 000000000000..56f112d43651 --- /dev/null +++ b/src/Http/Http.Extensions/test/JsonSerializerExtensionsTests.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using Microsoft.AspNetCore.Testing; +using Microsoft.DotNet.RemoteExecutor; + +namespace Microsoft.AspNetCore.Http.Extensions.Tests; + +public partial class JsonSerializerExtensionsTests +{ + [ConditionalFact] + [RemoteExecutionSupported] + public void Configure_ThrowsForNullTypeInfoResolver_WhenEnsureJsonTrimmabilityTrue() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var options = new JsonSerializerOptions(); + + // Act & Assert + Assert.Throws(() => JsonSerializerExtensions.Configure(options)); + }, options); + } + + [ConditionalFact] + [RemoteExecutionSupported] + public void Configure_Works_WhenEnsureJsonTrimmabilityTrue() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var options = new JsonSerializerOptions() { TypeInfoResolver = JsonSerializerExtensionsTestsContext.Default }; + + // Act + JsonSerializerExtensions.Configure(options); + + // Assert + Assert.NotNull(options.TypeInfoResolver); + Assert.IsType(options.TypeInfoResolver); + Assert.True(options.IsReadOnly); + }, options); + } + + [ConditionalFact] + [RemoteExecutionSupported] + public void DefaultSerializerOptions_Works_WhenEnsureJsonTrimmabilityFalse() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var options = new JsonSerializerOptions(); + + // Act + JsonSerializerExtensions.Configure(options); + + // Assert + Assert.NotNull(options.TypeInfoResolver); + Assert.IsType(options.TypeInfoResolver); + Assert.True(options.IsReadOnly); + }, options); + } + + [ConditionalFact] + [RemoteExecutionSupported] + public void DefaultSerializerOptions_Combines_WhenEnsureJsonTrimmabilityFalse() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var options = new JsonSerializerOptions() { TypeInfoResolver = JsonSerializerExtensionsTestsContext.Default }; + + // Act + JsonSerializerExtensions.Configure(options); + + // Assert + Assert.NotNull(options.TypeInfoResolver); + Assert.IsNotType(options.TypeInfoResolver); + Assert.IsNotType(options.TypeInfoResolver); + Assert.NotNull(options.TypeInfoResolver.GetTypeInfo(typeof(string), options)); + Assert.True(options.IsReadOnly); + }, options); + } + + [JsonSerializable(typeof(object))] + private partial class JsonSerializerExtensionsTestsContext : JsonSerializerContext + { } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 564da78c89f0..83a4b4495b77 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -20,6 +20,7 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; +using Microsoft.Extensions.Logging; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -28,13 +29,14 @@ 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; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Moq; -using Xunit.Abstractions; namespace Microsoft.AspNetCore.Routing.Internal; @@ -3131,6 +3133,7 @@ public async Task RequestDelegateWritesMembersFromChildTypesToJsonResponseBody_W var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) + .AddSingleton(Options.Create(new JsonOptions())) .BuildServiceProvider(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -3157,6 +3160,7 @@ public async Task RequestDelegateWritesMembersFromChildTypesToJsonResponseBody_W var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) + .AddSingleton(Options.Create(new JsonOptions())) .BuildServiceProvider(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -3183,6 +3187,7 @@ public async Task RequestDelegateWritesJsonTypeDiscriminatorToJsonResponseBody_W var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) + .AddSingleton(Options.Create(new JsonOptions())) .BuildServiceProvider(); var responseBodyStream = new MemoryStream(); @@ -3240,20 +3245,27 @@ public async Task RequestDelegateWritesAsJsonResponseBody_WithJsonSerializerCont Assert.Equal("Write even more tests!", deserializedResponseBody!.Name); } - [Fact] - public void CreateDelegateThrows_WhenGetJsonTypeInfoFail() + [ConditionalFact] + [RemoteExecutionSupported] + public void CreateDelegateThrows_WhenGetJsonTypeInfoFail_AndEnsureJsonTrimmabilityTrue() { - var httpContext = CreateHttpContext(); - httpContext.RequestServices = new ServiceCollection() - .AddSingleton(LoggerFactory) - .ConfigureHttpJsonOptions(o => o.SerializerOptions.TypeInfoResolver = TestJsonContext.Default) - .BuildServiceProvider(); + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + var httpContext = CreateDefaultHttpContext(); + httpContext.RequestServices = new ServiceCollection() + .AddSingleton(NullLoggerFactory.Instance) + .ConfigureHttpJsonOptions(o => o.SerializerOptions.TypeInfoResolver = TestJsonContext.Default) + .BuildServiceProvider(); + + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - TodoStruct TestAction() => new TodoStruct(42, "Bob", true); - Assert.Throws(() => RequestDelegateFactory.Create(TestAction, new() { ServiceProvider = httpContext.RequestServices })); + TodoStruct TestAction() => new TodoStruct(42, "Bob", true); + Assert.Throws(() => RequestDelegateFactory.Create(TestAction, new() { ServiceProvider = httpContext.RequestServices })); + }, options); } public static IEnumerable CustomResults @@ -7419,12 +7431,18 @@ static void TestAction([AsParameters] ParameterListMixedRequiredStringsFromDiffe #nullable enable private DefaultHttpContext CreateHttpContext() + { + var context = CreateDefaultHttpContext(); + context.RequestServices = new ServiceCollection().AddSingleton(LoggerFactory).BuildServiceProvider(); + return context; + } + + private static DefaultHttpContext CreateDefaultHttpContext() { var responseFeature = new TestHttpResponseFeature(); return new() { - RequestServices = new ServiceCollection().AddSingleton(LoggerFactory).BuildServiceProvider(), Features = { [typeof(IHttpResponseFeature)] = responseFeature, diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index 549d49b0231f..e3528ffa27fd 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -37,6 +37,10 @@ + + + + diff --git a/src/Http/Routing/src/Properties/ILLink.Substitutions.xml b/src/Http/Routing/src/Properties/ILLink.Substitutions.xml new file mode 100644 index 000000000000..3beb4a06f235 --- /dev/null +++ b/src/Http/Routing/src/Properties/ILLink.Substitutions.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs index d7fc88d828e9..4cb13706a8ce 100644 --- a/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs +++ b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs @@ -8,6 +8,7 @@ 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; @@ -199,13 +200,30 @@ public async Task WriteResponseBodyAsync_UsesJsonPolymorphismOptions() } [Fact] - public void WriteResponseBodyAsync_Throws_WhenTypeResolverIsNull() + public void CreateFormatter_DoesNotThrow_WhenTypeResolverIsNull() { // Arrange var jsonOptions = new JsonOptions(); jsonOptions.JsonSerializerOptions.TypeInfoResolver = null; - Assert.Throws(() => SystemTextJsonOutputFormatter.CreateFormatter(jsonOptions)); + Assert.IsType< SystemTextJsonOutputFormatter>(SystemTextJsonOutputFormatter.CreateFormatter(jsonOptions)); + } + + [Fact] + public void CreateFormatter_Throws_WhenTypeResolverIsNull_AndEnsureJsonTrimmabilityTrue() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var jsonOptions = new JsonOptions(); + jsonOptions.JsonSerializerOptions.TypeInfoResolver = null; + + Assert.Throws(() => SystemTextJsonOutputFormatter.CreateFormatter(jsonOptions)); + + }, options); } private class Person diff --git a/src/Mvc/Mvc.Core/test/Infrastructure/ValidationProblemDetailsJsonConverterTest.cs b/src/Mvc/Mvc.Core/test/Infrastructure/ValidationProblemDetailsJsonConverterTest.cs index df3ef0d94cd3..b189bbb421e5 100644 --- a/src/Mvc/Mvc.Core/test/Infrastructure/ValidationProblemDetailsJsonConverterTest.cs +++ b/src/Mvc/Mvc.Core/test/Infrastructure/ValidationProblemDetailsJsonConverterTest.cs @@ -3,12 +3,16 @@ using System.Text; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; namespace Microsoft.AspNetCore.Mvc.Infrastructure; public class ValidationProblemDetailsJsonConverterTest { - private static JsonSerializerOptions JsonSerializerOptions => new JsonOptions().JsonSerializerOptions; + private static JsonSerializerOptions JsonSerializerOptions => new JsonSerializerOptions(new JsonOptions().JsonSerializerOptions) + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver() + }; [Fact] public void Read_Works() diff --git a/src/Mvc/Mvc.Core/test/JsonOptionsTest.cs b/src/Mvc/Mvc.Core/test/JsonOptionsTest.cs deleted file mode 100644 index 05209836bd45..000000000000 --- a/src/Mvc/Mvc.Core/test/JsonOptionsTest.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Text.Json.Serialization.Metadata; -using Microsoft.AspNetCore.Testing; -using Microsoft.DotNet.RemoteExecutor; - -namespace Microsoft.AspNetCore.Mvc; - -public class JsonOptionsTest -{ - [ConditionalFact] - [RemoteExecutionSupported] - public void DefaultSerializerOptions_SetsTypeInfoResolverNull_WhenEnsureJsonTrimmabilityTrue() - { - var options = new RemoteInvokeOptions(); - options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); - - using var remoteHandle = RemoteExecutor.Invoke(static () => - { - // Arrange - var options = new JsonOptions().JsonSerializerOptions; - - // Assert - Assert.Null(options.TypeInfoResolver); - }, options); - } - - [ConditionalFact] - [RemoteExecutionSupported] - public void DefaultSerializerOptions_SetsTypeInfoResolverToDefault_WhenEnsureJsonTrimmabilityFalse() - { - var options = new RemoteInvokeOptions(); - options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); - - using var remoteHandle = RemoteExecutor.Invoke(static () => - { - // Arrange - var options = new JsonOptions().JsonSerializerOptions; - - // Assert - Assert.NotNull(options.TypeInfoResolver); - Assert.IsType(options.TypeInfoResolver); - }, options); - } -} diff --git a/src/Shared/Json/JsonSerializerExtensions.cs b/src/Shared/Json/JsonSerializerExtensions.cs index f896b3259309..f7973389693f 100644 --- a/src/Shared/Json/JsonSerializerExtensions.cs +++ b/src/Shared/Json/JsonSerializerExtensions.cs @@ -41,6 +41,11 @@ public static void Configure(this JsonSerializerOptions options) [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] private static void InitializeForReflection(JsonSerializerOptions options) { - options.TypeInfoResolver = JsonTypeInfoResolver.Combine(options.TypeInfoResolver, new DefaultJsonTypeInfoResolver()); + var combinedResolver = JsonTypeInfoResolver.Combine(options.TypeInfoResolver, new DefaultJsonTypeInfoResolver()); + + if (!options.IsReadOnly) + { + options.TypeInfoResolver = combinedResolver; + } } } From e186cc4c9ac2d3246fa3f3b6f374a9825ca68e5d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Feb 2023 13:31:32 -0800 Subject: [PATCH 17/48] Include TrimmingAppContextSwitches --- .../Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj index 3473e085dfaf..5695bf0df2b4 100644 --- a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj +++ b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj @@ -20,6 +20,7 @@ + From de59eb6ea1e4e3c2d00b95de5cccf49bc8065a46 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 2 Feb 2023 08:53:31 -0800 Subject: [PATCH 18/48] Create Microsoft.AspNetCore.Http.Results.WarningSuppressions.xml --- ...t.AspNetCore.Http.Results.WarningSuppressions.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.WarningSuppressions.xml diff --git a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.WarningSuppressions.xml b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.WarningSuppressions.xml new file mode 100644 index 000000000000..b6b9dd688fe7 --- /dev/null +++ b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.WarningSuppressions.xml @@ -0,0 +1,12 @@ + + + + + ILLink + IL2026 + member + M:Microsoft.AspNetCore.Http.JsonSerializerExtensions.Configure(System.Text.Json.JsonSerializerOptions) + This warning is left in the product so developers get an ILLink warning when trimming an app only when Microsoft.AspNetCore.EnsureJsonTrimmability=false. + + + \ No newline at end of file From 2f2869a9e728fd4497f0d5210f11f454c2a7147f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 2 Feb 2023 09:00:58 -0800 Subject: [PATCH 19/48] Clean up --- src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 83a4b4495b77..5dcf83e38f2a 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -32,7 +32,6 @@ using Microsoft.DotNet.RemoteExecutor; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; From c45a8f356949a9de6dc85a90ec6d8f9cb270d492 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 3 Feb 2023 11:08:38 -0800 Subject: [PATCH 20/48] Merging changes from #46225 --- .../src/ProblemDetailsJsonOptionsSetup.cs | 20 +++++++------ ...mDetailsServiceCollectionExtensionsTest.cs | 30 +++++++++++++++---- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/Http/Http.Extensions/src/ProblemDetailsJsonOptionsSetup.cs b/src/Http/Http.Extensions/src/ProblemDetailsJsonOptionsSetup.cs index b2fd02ba260e..a574bd9e90da 100644 --- a/src/Http/Http.Extensions/src/ProblemDetailsJsonOptionsSetup.cs +++ b/src/Http/Http.Extensions/src/ProblemDetailsJsonOptionsSetup.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Http.Json; using Microsoft.Extensions.Options; @@ -11,15 +10,18 @@ internal sealed class ProblemDetailsJsonOptionsSetup : IPostConfigureOptions(); + break; + default: + break; } } } diff --git a/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs index bb0d16c9b039..5199842de795 100644 --- a/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; @@ -102,7 +103,7 @@ public void AddProblemDetails_CombinesProblemDetailsContext() } [Fact] - public void AddProblemDetails_CombinesProblemDetailsContext_ForReadOnlyJsonOptions() + public void AddProblemDetails_Throws_ForReadOnlyJsonOptions() { // Arrange var collection = new ServiceCollection(); @@ -119,10 +120,7 @@ public void AddProblemDetails_CombinesProblemDetailsContext_ForReadOnlyJsonOptio var services = collection.BuildServiceProvider(); var jsonOptions = services.GetService>(); - Assert.NotNull(jsonOptions.Value); - Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver); - Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver.GetTypeInfo(typeof(ProblemDetails), jsonOptions.Value.SerializerOptions)); - Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver.GetTypeInfo(typeof(TypeA), jsonOptions.Value.SerializerOptions)); + Assert.Throws(() => jsonOptions.Value); } [Fact] @@ -165,6 +163,28 @@ public void AddProblemDetails_DoesNotCombineProblemDetailsContext_WhenNullTypeIn Assert.Null(jsonOptions.Value.SerializerOptions.TypeInfoResolver); } + [Fact] + public void AddProblemDetails_CombineProblemDetailsContext_WhenDefaultypeInfoResolver() + { + // Arrange + var collection = new ServiceCollection(); + collection.AddOptions(); + collection.ConfigureAll(options => options.SerializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver()); + + // Act + collection.AddProblemDetails(); + + // Assert + var services = collection.BuildServiceProvider(); + var jsonOptions = services.GetService>(); + + Assert.NotNull(jsonOptions.Value); + Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver); + Assert.IsNotType(jsonOptions.Value.SerializerOptions.TypeInfoResolver); + Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver.GetTypeInfo(typeof(ProblemDetails), jsonOptions.Value.SerializerOptions)); + Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver.GetTypeInfo(typeof(TypeA), jsonOptions.Value.SerializerOptions)); + } + [JsonSerializable(typeof(TypeA))] internal partial class TestExtensionsJsonContext : JsonSerializerContext { } From de450ae19ed1b755a922fa72262fb6deeabcd098 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 3 Feb 2023 11:13:16 -0800 Subject: [PATCH 21/48] Locking JsonOptions :( --- .../test/JsonSerializerExtensionsTests.cs | 8 +++---- .../Http.Results/src/HttpResultsHelper.cs | 2 ++ .../SystemTextJsonOutputFormatter.cs | 3 ++- src/Shared/Json/JsonSerializerExtensions.cs | 22 ++++++++++++++----- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/Http/Http.Extensions/test/JsonSerializerExtensionsTests.cs b/src/Http/Http.Extensions/test/JsonSerializerExtensionsTests.cs index 56f112d43651..e3ea3ad98a6d 100644 --- a/src/Http/Http.Extensions/test/JsonSerializerExtensionsTests.cs +++ b/src/Http/Http.Extensions/test/JsonSerializerExtensionsTests.cs @@ -24,7 +24,7 @@ public void Configure_ThrowsForNullTypeInfoResolver_WhenEnsureJsonTrimmabilityTr var options = new JsonSerializerOptions(); // Act & Assert - Assert.Throws(() => JsonSerializerExtensions.Configure(options)); + Assert.Throws(() => JsonSerializerExtensions.EnsureConfigured(options)); }, options); } @@ -41,7 +41,7 @@ public void Configure_Works_WhenEnsureJsonTrimmabilityTrue() var options = new JsonSerializerOptions() { TypeInfoResolver = JsonSerializerExtensionsTestsContext.Default }; // Act - JsonSerializerExtensions.Configure(options); + JsonSerializerExtensions.EnsureConfigured(options); // Assert Assert.NotNull(options.TypeInfoResolver); @@ -63,7 +63,7 @@ public void DefaultSerializerOptions_Works_WhenEnsureJsonTrimmabilityFalse() var options = new JsonSerializerOptions(); // Act - JsonSerializerExtensions.Configure(options); + JsonSerializerExtensions.EnsureConfigured(options); // Assert Assert.NotNull(options.TypeInfoResolver); @@ -85,7 +85,7 @@ public void DefaultSerializerOptions_Combines_WhenEnsureJsonTrimmabilityFalse() var options = new JsonSerializerOptions() { TypeInfoResolver = JsonSerializerExtensionsTestsContext.Default }; // Act - JsonSerializerExtensions.Configure(options); + JsonSerializerExtensions.EnsureConfigured(options); // Assert Assert.NotNull(options.TypeInfoResolver); diff --git a/src/Http/Http.Results/src/HttpResultsHelper.cs b/src/Http/Http.Results/src/HttpResultsHelper.cs index d3a23dcfa68c..8b95be0a525f 100644 --- a/src/Http/Http.Results/src/HttpResultsHelper.cs +++ b/src/Http/Http.Results/src/HttpResultsHelper.cs @@ -32,6 +32,8 @@ public static Task WriteResultAsJsonAsync( } jsonSerializerOptions ??= ResolveJsonOptions(httpContext).SerializerOptions; + jsonSerializerOptions.EnsureConfigured(); + var jsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(TValue)); Type? runtimeType; diff --git a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs index 8d0d10303e78..7e49e66abf89 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs @@ -21,7 +21,8 @@ public class SystemTextJsonOutputFormatter : TextOutputFormatter /// The . public SystemTextJsonOutputFormatter(JsonSerializerOptions jsonSerializerOptions) { - jsonSerializerOptions.Configure(); + jsonSerializerOptions.EnsureConfigured(); + SerializerOptions = jsonSerializerOptions; SupportedEncodings.Add(Encoding.UTF8); diff --git a/src/Shared/Json/JsonSerializerExtensions.cs b/src/Shared/Json/JsonSerializerExtensions.cs index 57e5c7854a8d..41c880a9081c 100644 --- a/src/Shared/Json/JsonSerializerExtensions.cs +++ b/src/Shared/Json/JsonSerializerExtensions.cs @@ -11,6 +11,8 @@ namespace Microsoft.AspNetCore.Http; internal static class JsonSerializerExtensions { + private static DefaultJsonTypeInfoResolver? _defaultJsonTypeInfoResolver; + public static bool HasKnownPolymorphism(this JsonTypeInfo jsonTypeInfo) => jsonTypeInfo.Type.IsSealed || jsonTypeInfo.Type.IsValueType || jsonTypeInfo.PolymorphismOptions is not null; @@ -19,12 +21,12 @@ public static bool IsValid(this JsonTypeInfo jsonTypeInfo, [NotNullWhen(false)] public static JsonTypeInfo GetReadOnlyTypeInfo(this JsonSerializerOptions options, Type type) { - options.Configure(); + options.EnsureConfigured(); return options.GetTypeInfo(type); } - public static void Configure(this JsonSerializerOptions options) + public static void EnsureConfigured(this JsonSerializerOptions options) { if (!options.IsReadOnly) { @@ -45,11 +47,19 @@ public static void Configure(this JsonSerializerOptions options) [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] private static void InitializeForReflection(JsonSerializerOptions options) { - var combinedResolver = JsonTypeInfoResolver.Combine(options.TypeInfoResolver, new DefaultJsonTypeInfoResolver()); - - if (!options.IsReadOnly) + // TODO: How can I avoid this lock? + lock (options) { - options.TypeInfoResolver = combinedResolver; + if (!options.IsReadOnly) + { + _defaultJsonTypeInfoResolver ??= new DefaultJsonTypeInfoResolver(); + + options.TypeInfoResolver = options.TypeInfoResolver switch + { + null => _defaultJsonTypeInfoResolver, + _ => JsonTypeInfoResolver.Combine(options.TypeInfoResolver, _defaultJsonTypeInfoResolver), + }; + } } } public static JsonTypeInfo GetRequiredTypeInfo(this JsonSerializerContext context, Type type) From b3b7cefb45c01d46ddaf20a9d0c69e464d6350fd Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 3 Feb 2023 11:52:31 -0800 Subject: [PATCH 22/48] Fixing linker suppressions --- ...crosoft.AspNetCore.Http.Extensions.WarningSuppressions.xml | 4 ++-- .../Microsoft.AspNetCore.Http.Results.WarningSuppressions.xml | 4 ++-- .../src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml index a807f7ba044c..8d5e4570e6b9 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml @@ -5,8 +5,8 @@ ILLink IL2026 member - M:Microsoft.AspNetCore.Http.JsonSerializerExtensions.Configure(System.Text.Json.JsonSerializerOptions) + M:Microsoft.AspNetCore.Http.JsonSerializerExtensions.EnsureConfigured(System.Text.Json.JsonSerializerOptions) This warning is left in the product so developers get an ILLink warning when trimming an app only when Microsoft.AspNetCore.EnsureJsonTrimmability=false. - \ No newline at end of file + diff --git a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.WarningSuppressions.xml b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.WarningSuppressions.xml index b6b9dd688fe7..1531fc858fba 100644 --- a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.WarningSuppressions.xml +++ b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.WarningSuppressions.xml @@ -5,8 +5,8 @@ ILLink IL2026 member - M:Microsoft.AspNetCore.Http.JsonSerializerExtensions.Configure(System.Text.Json.JsonSerializerOptions) + M:Microsoft.AspNetCore.Http.JsonSerializerExtensions.EnsureConfigured(System.Text.Json.JsonSerializerOptions) This warning is left in the product so developers get an ILLink warning when trimming an app only when Microsoft.AspNetCore.EnsureJsonTrimmability=false. - \ No newline at end of file + diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml index 6a75f00c1603..a675f48d5253 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml @@ -5,8 +5,8 @@ ILLink IL2026 member - M:Microsoft.AspNetCore.Http.JsonSerializerExtensions.Configure(System.Text.Json.JsonSerializerOptions) + M:Microsoft.AspNetCore.Http.JsonSerializerExtensions.EnsureConfigured(System.Text.Json.JsonSerializerOptions) This warning is left in the product so developers get an ILLink warning when trimming an app only when Microsoft.AspNetCore.EnsureJsonTrimmability=false. - \ No newline at end of file + From 5715427ef52c9adb88328b1800b3c7a186068012 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 6 Feb 2023 13:46:06 -0800 Subject: [PATCH 23/48] Simplify and using PostConfigureOptions --- src/DefaultBuilder/src/WebHost.cs | 3 + .../src/DefaultHttpJsonOptionsSetup.cs | 15 ++ .../src/HttpJsonServiceExtensions.cs | 25 ++- .../src/HttpRequestJsonExtensions.cs | 6 +- .../src/HttpResponseJsonExtensions.cs | 6 +- src/Http/Http.Extensions/src/JsonOptions.cs | 34 +++- ...re.Http.Extensions.WarningSuppressions.xml | 4 +- .../src/PublicAPI.Unshipped.txt | 2 + .../src/RequestDelegateFactory.cs | 17 +- .../test/JsonSerializerExtensionsTests.cs | 168 +++++++++--------- .../test/RequestDelegateFactoryTests.cs | 6 +- .../Http.Results/src/HttpResultsHelper.cs | 3 +- ...tCore.Http.Results.WarningSuppressions.xml | 12 -- .../Microsoft.AspNetCore.Http.Results.csproj | 2 +- .../test/AcceptedAtRouteOfTResultTests.cs | 1 + .../test/AcceptedOfTResultTests.cs | 1 + .../test/HttpResultsHelperTests.cs | 21 +++ ...AspNetCore.Routing.WarningSuppressions.xml | 12 -- .../RequestDelegateFilterPipelineBuilder.cs | 6 +- .../test/FunctionalTests/RouteHandlerTest.cs | 1 + .../DeveloperExceptionPageMiddlewareImpl.cs | 2 +- .../MvcCoreServiceCollectionExtensions.cs | 1 + .../SystemTextJsonOutputFormatter.cs | 4 +- src/Mvc/Mvc.Core/src/JsonOptions.cs | 17 ++ src/Shared/Json/JsonSerializerExtensions.cs | 49 +---- 25 files changed, 239 insertions(+), 179 deletions(-) create mode 100644 src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs delete mode 100644 src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.WarningSuppressions.xml delete mode 100644 src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml diff --git a/src/DefaultBuilder/src/WebHost.cs b/src/DefaultBuilder/src/WebHost.cs index 4a49c3ef8e3c..331757777e98 100644 --- a/src/DefaultBuilder/src/WebHost.cs +++ b/src/DefaultBuilder/src/WebHost.cs @@ -258,6 +258,9 @@ internal static void UseKestrel(IWebHostBuilder builder) services.AddTransient, ForwardedHeadersOptionsSetup>(); services.AddRouting(); + + // JsonOptions + services.AddDefaultHttpJsonOptions(); }); } diff --git a/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs b/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs new file mode 100644 index 000000000000..05c60913d375 --- /dev/null +++ b/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.Json; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Http; + +internal sealed class DefaultHttpJsonOptionsSetup : IPostConfigureOptions +{ + public void PostConfigure(string? name, JsonOptions options) + { + options.EnsureConfigured(markAsReadOnly: false); + } +} diff --git a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs index c5ad6b8071bc..184681ce2649 100644 --- a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs @@ -1,7 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Json; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection; @@ -22,7 +25,27 @@ public static class HttpJsonServiceExtensions /// The modified . public static IServiceCollection ConfigureHttpJsonOptions(this IServiceCollection services, Action configureOptions) { - services.Configure(configureOptions); + services.AddDefaultHttpJsonOptions(); + services.Configure(configureOptions); + + return services; + } + + /// + /// Configures the default options used for reading and writing JSON when using + /// + /// and . + /// uses default values from JsonSerializerDefaults.Web. + /// + /// The to configure options on. + /// The modified . + public static IServiceCollection AddDefaultHttpJsonOptions(this IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + + services.AddOptions(); + services.TryAddEnumerable(ServiceDescriptor.Singleton, DefaultHttpJsonOptionsSetup>()); + return services; } } diff --git a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs index 8d0a08a7be4e..9333635a802e 100644 --- a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs @@ -39,7 +39,7 @@ public static class HttpRequestJsonExtensions ArgumentNullException.ThrowIfNull(request); var options = ResolveSerializerOptions(request.HttpContext); - return request.ReadFromJsonAsync(jsonTypeInfo: (JsonTypeInfo)options.GetReadOnlyTypeInfo(typeof(TValue)), cancellationToken); + return request.ReadFromJsonAsync(jsonTypeInfo: (JsonTypeInfo)options.GetTypeInfo(typeof(TValue)), cancellationToken); } /// @@ -175,7 +175,7 @@ public static class HttpRequestJsonExtensions ArgumentNullException.ThrowIfNull(request); var options = ResolveSerializerOptions(request.HttpContext); - return request.ReadFromJsonAsync(jsonTypeInfo: options.GetReadOnlyTypeInfo(type), cancellationToken); + return request.ReadFromJsonAsync(jsonTypeInfo: options.GetTypeInfo(type), cancellationToken); } /// @@ -303,7 +303,7 @@ private static bool HasJsonContentType(this HttpRequest request, out StringSegme private static JsonSerializerOptions ResolveSerializerOptions(HttpContext httpContext) { // Attempt to resolve options from DI then fallback to default options - return httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; + return httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions ?? JsonOptions.Default.SerializerOptions; } [DoesNotReturn] diff --git a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs index e6903d8725f2..218ceb9959d3 100644 --- a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs @@ -38,7 +38,7 @@ public static Task WriteAsJsonAsync( ArgumentNullException.ThrowIfNull(response); var options = ResolveSerializerOptions(response.HttpContext); - return response.WriteAsJsonAsync(value, jsonTypeInfo: (JsonTypeInfo)options.GetReadOnlyTypeInfo(typeof(TValue)), contentType: null, cancellationToken); + return response.WriteAsJsonAsync(value, jsonTypeInfo: (JsonTypeInfo)options.GetTypeInfo(typeof(TValue)), contentType: null, cancellationToken); } /// @@ -213,7 +213,7 @@ public static Task WriteAsJsonAsync( ArgumentNullException.ThrowIfNull(response); var options = ResolveSerializerOptions(response.HttpContext); - return response.WriteAsJsonAsync(value, jsonTypeInfo: options.GetReadOnlyTypeInfo(type), contentType: null, cancellationToken); + return response.WriteAsJsonAsync(value, jsonTypeInfo: options.GetTypeInfo(type), contentType: null, cancellationToken); } /// @@ -339,6 +339,6 @@ async Task WriteAsJsonAsyncSlow() private static JsonSerializerOptions ResolveSerializerOptions(HttpContext httpContext) { // Attempt to resolve options from DI then fallback to default options - return httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; + return httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions ?? JsonOptions.Default.SerializerOptions; } } diff --git a/src/Http/Http.Extensions/src/JsonOptions.cs b/src/Http/Http.Extensions/src/JsonOptions.cs index 4b4296d38584..dfd572b464fe 100644 --- a/src/Http/Http.Extensions/src/JsonOptions.cs +++ b/src/Http/Http.Extensions/src/JsonOptions.cs @@ -3,6 +3,7 @@ using System.Text.Encodings.Web; using System.Text.Json; +using Microsoft.AspNetCore.Internal; #nullable enable @@ -14,7 +15,9 @@ namespace Microsoft.AspNetCore.Http.Json; /// public class JsonOptions { - internal static readonly JsonSerializerOptions DefaultSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) + private static JsonOptions? _defaultInstance; + + private static readonly JsonSerializerOptions DefaultSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) { // Web defaults don't use the relex JSON escaping encoder. // @@ -27,5 +30,32 @@ public class JsonOptions /// /// Gets the . /// - public JsonSerializerOptions SerializerOptions { get; internal set; } = new JsonSerializerOptions(DefaultSerializerOptions); + public JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions(DefaultSerializerOptions); + + /// + /// Gets the default instance. + /// + public static JsonOptions Default { get => _defaultInstance ??= new JsonOptions().EnsureConfigured(); } + + internal JsonOptions EnsureConfigured(bool markAsReadOnly = true) + { + if (!SerializerOptions.IsReadOnly) + { + if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) + { +#pragma warning disable IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml +#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. + SerializerOptions.InitializeForReflection(); +#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. +#pragma warning restore IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml + } + + if (markAsReadOnly) + { + SerializerOptions.MakeReadOnly(); + } + } + + return this; + } } diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml index 8d5e4570e6b9..924b3ec9949e 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml @@ -5,8 +5,8 @@ ILLink IL2026 member - M:Microsoft.AspNetCore.Http.JsonSerializerExtensions.EnsureConfigured(System.Text.Json.JsonSerializerOptions) + M:Microsoft.AspNetCore.Http.Json.JsonOptions.EnsureConfigured(System.Boolean) This warning is left in the product so developers get an ILLink warning when trimming an app only when Microsoft.AspNetCore.EnsureJsonTrimmability=false. - + \ No newline at end of file diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index 254e8dfb2161..5b837db4c608 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ #nullable enable static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, object? value, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, string? contentType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +static Microsoft.AspNetCore.Http.Json.JsonOptions.Default.get -> Microsoft.AspNetCore.Http.Json.JsonOptions! +static Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.AddDefaultHttpJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 08f8bacf5a82..41c7523a02d5 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -262,7 +262,8 @@ private static RequestDelegateFactoryContext CreateFactoryContext(RequestDelegat var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices ?? EmptyServiceProvider.Instance; var endpointBuilder = options?.EndpointBuilder ?? new RdfEndpointBuilder(serviceProvider); - var jsonSerializerOptions = serviceProvider.GetService>()?.Value.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; + var jsonSerializerOptions = serviceProvider.GetService>()?.Value.SerializerOptions ?? JsonOptions.Default.SerializerOptions; + jsonSerializerOptions.MakeReadOnly(); var factoryContext = new RequestDelegateFactoryContext { @@ -1001,7 +1002,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, methodCall, HttpContextExpr, factoryContext.JsonSerializerOptionsExpression, - Expression.Constant(factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeof(object)), typeof(JsonTypeInfo))); + Expression.Constant(factoryContext.JsonSerializerOptions.GetTypeInfo(typeof(object)), typeof(JsonTypeInfo))); } else if (returnType == typeof(ValueTask)) { @@ -1009,7 +1010,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, methodCall, HttpContextExpr, factoryContext.JsonSerializerOptionsExpression, - Expression.Constant(factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeof(object)), typeof(JsonTypeInfo))); + Expression.Constant(factoryContext.JsonSerializerOptions.GetTypeInfo(typeof(object)), typeof(JsonTypeInfo))); } else if (returnType == typeof(Task)) { @@ -1017,7 +1018,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, methodCall, HttpContextExpr, factoryContext.JsonSerializerOptionsExpression, - Expression.Constant(factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeof(object)), typeof(JsonTypeInfo))); + Expression.Constant(factoryContext.JsonSerializerOptions.GetTypeInfo(typeof(object)), typeof(JsonTypeInfo))); } else if (AwaitableInfo.IsTypeAwaitable(returnType, out _)) { @@ -1053,7 +1054,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, } else { - var jsonTypeInfo = factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeArg); + var jsonTypeInfo = factoryContext.JsonSerializerOptions.GetTypeInfo(typeArg); if (jsonTypeInfo.HasKnownPolymorphism()) { @@ -1094,7 +1095,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, } else { - var jsonTypeInfo = factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeArg); + var jsonTypeInfo = factoryContext.JsonSerializerOptions.GetTypeInfo(typeArg); if (jsonTypeInfo.HasKnownPolymorphism()) { @@ -1138,7 +1139,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, } else { - var jsonTypeInfo = factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(returnType); + var jsonTypeInfo = factoryContext.JsonSerializerOptions.GetTypeInfo(returnType); if (jsonTypeInfo.HasKnownPolymorphism()) { @@ -1205,7 +1206,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, Debug.Assert(factoryContext.JsonRequestBodyParameter is not null, "factoryContext.JsonRequestBodyParameter is null for a JSON body."); var bodyType = factoryContext.JsonRequestBodyParameter.ParameterType; - var jsonTypeInfo = factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(bodyType); + var jsonTypeInfo = factoryContext.JsonSerializerOptions.GetTypeInfo(bodyType); var parameterTypeName = TypeNameHelper.GetTypeDisplayName(factoryContext.JsonRequestBodyParameter.ParameterType, fullName: false); var parameterName = factoryContext.JsonRequestBodyParameter.Name; diff --git a/src/Http/Http.Extensions/test/JsonSerializerExtensionsTests.cs b/src/Http/Http.Extensions/test/JsonSerializerExtensionsTests.cs index e3ea3ad98a6d..af75310a7286 100644 --- a/src/Http/Http.Extensions/test/JsonSerializerExtensionsTests.cs +++ b/src/Http/Http.Extensions/test/JsonSerializerExtensionsTests.cs @@ -11,90 +11,90 @@ namespace Microsoft.AspNetCore.Http.Extensions.Tests; public partial class JsonSerializerExtensionsTests { - [ConditionalFact] - [RemoteExecutionSupported] - public void Configure_ThrowsForNullTypeInfoResolver_WhenEnsureJsonTrimmabilityTrue() - { - var options = new RemoteInvokeOptions(); - options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); - - using var remoteHandle = RemoteExecutor.Invoke(static () => - { - // Arrange - var options = new JsonSerializerOptions(); - - // Act & Assert - Assert.Throws(() => JsonSerializerExtensions.EnsureConfigured(options)); - }, options); - } - - [ConditionalFact] - [RemoteExecutionSupported] - public void Configure_Works_WhenEnsureJsonTrimmabilityTrue() - { - var options = new RemoteInvokeOptions(); - options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); - - using var remoteHandle = RemoteExecutor.Invoke(static () => - { - // Arrange - var options = new JsonSerializerOptions() { TypeInfoResolver = JsonSerializerExtensionsTestsContext.Default }; - - // Act - JsonSerializerExtensions.EnsureConfigured(options); - - // Assert - Assert.NotNull(options.TypeInfoResolver); - Assert.IsType(options.TypeInfoResolver); - Assert.True(options.IsReadOnly); - }, options); - } - - [ConditionalFact] - [RemoteExecutionSupported] - public void DefaultSerializerOptions_Works_WhenEnsureJsonTrimmabilityFalse() - { - var options = new RemoteInvokeOptions(); - options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); - - using var remoteHandle = RemoteExecutor.Invoke(static () => - { - // Arrange - var options = new JsonSerializerOptions(); - - // Act - JsonSerializerExtensions.EnsureConfigured(options); - - // Assert - Assert.NotNull(options.TypeInfoResolver); - Assert.IsType(options.TypeInfoResolver); - Assert.True(options.IsReadOnly); - }, options); - } - - [ConditionalFact] - [RemoteExecutionSupported] - public void DefaultSerializerOptions_Combines_WhenEnsureJsonTrimmabilityFalse() - { - var options = new RemoteInvokeOptions(); - options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); - - using var remoteHandle = RemoteExecutor.Invoke(static () => - { - // Arrange - var options = new JsonSerializerOptions() { TypeInfoResolver = JsonSerializerExtensionsTestsContext.Default }; - - // Act - JsonSerializerExtensions.EnsureConfigured(options); - - // Assert - Assert.NotNull(options.TypeInfoResolver); - Assert.IsNotType(options.TypeInfoResolver); - Assert.IsNotType(options.TypeInfoResolver); - Assert.NotNull(options.TypeInfoResolver.GetTypeInfo(typeof(string), options)); - Assert.True(options.IsReadOnly); - }, options); - } + //[ConditionalFact] + //[RemoteExecutionSupported] + //public void Configure_ThrowsForNullTypeInfoResolver_WhenEnsureJsonTrimmabilityTrue() + //{ + // var options = new RemoteInvokeOptions(); + // options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); + + // using var remoteHandle = RemoteExecutor.Invoke(static () => + // { + // // Arrange + // var options = new JsonSerializerOptions(); + + // // Act & Assert + // Assert.Throws(() => JsonSerializerExtensions.EnsureConfigured(options)); + // }, options); + //} + + //[ConditionalFact] + //[RemoteExecutionSupported] + //public void Configure_Works_WhenEnsureJsonTrimmabilityTrue() + //{ + // var options = new RemoteInvokeOptions(); + // options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); + + // using var remoteHandle = RemoteExecutor.Invoke(static () => + // { + // // Arrange + // var options = new JsonSerializerOptions() { TypeInfoResolver = JsonSerializerExtensionsTestsContext.Default }; + + // // Act + // JsonSerializerExtensions.EnsureConfigured(options); + + // // Assert + // Assert.NotNull(options.TypeInfoResolver); + // Assert.IsType(options.TypeInfoResolver); + // Assert.True(options.IsReadOnly); + // }, options); + //} + + //[ConditionalFact] + //[RemoteExecutionSupported] + //public void DefaultSerializerOptions_Works_WhenEnsureJsonTrimmabilityFalse() + //{ + // var options = new RemoteInvokeOptions(); + // options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); + + // using var remoteHandle = RemoteExecutor.Invoke(static () => + // { + // // Arrange + // var options = new JsonSerializerOptions(); + + // // Act + // JsonSerializerExtensions.EnsureConfigured(options); + + // // Assert + // Assert.NotNull(options.TypeInfoResolver); + // Assert.IsType(options.TypeInfoResolver); + // Assert.True(options.IsReadOnly); + // }, options); + //} + + //[ConditionalFact] + //[RemoteExecutionSupported] + //public void DefaultSerializerOptions_Combines_WhenEnsureJsonTrimmabilityFalse() + //{ + // var options = new RemoteInvokeOptions(); + // options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); + + // using var remoteHandle = RemoteExecutor.Invoke(static () => + // { + // // Arrange + // var options = new JsonSerializerOptions() { TypeInfoResolver = JsonSerializerExtensionsTestsContext.Default }; + + // // Act + // JsonSerializerExtensions.EnsureConfigured(options); + + // // Assert + // Assert.NotNull(options.TypeInfoResolver); + // Assert.IsNotType(options.TypeInfoResolver); + // Assert.IsNotType(options.TypeInfoResolver); + // Assert.NotNull(options.TypeInfoResolver.GetTypeInfo(typeof(string), options)); + // Assert.True(options.IsReadOnly); + // }, options); + //} [JsonSerializable(typeof(object))] private partial class JsonSerializerExtensionsTestsContext : JsonSerializerContext diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 5dcf83e38f2a..faee0bf6faac 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -3132,7 +3132,7 @@ public async Task RequestDelegateWritesMembersFromChildTypesToJsonResponseBody_W var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) - .AddSingleton(Options.Create(new JsonOptions())) + .AddSingleton(Options.Create(JsonOptions.Default)) .BuildServiceProvider(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -3159,7 +3159,7 @@ public async Task RequestDelegateWritesMembersFromChildTypesToJsonResponseBody_W var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) - .AddSingleton(Options.Create(new JsonOptions())) + .AddSingleton(Options.Create(JsonOptions.Default)) .BuildServiceProvider(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -3186,7 +3186,7 @@ public async Task RequestDelegateWritesJsonTypeDiscriminatorToJsonResponseBody_W var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) - .AddSingleton(Options.Create(new JsonOptions())) + .AddSingleton(Options.Create(JsonOptions.Default)) .BuildServiceProvider(); var responseBodyStream = new MemoryStream(); diff --git a/src/Http/Http.Results/src/HttpResultsHelper.cs b/src/Http/Http.Results/src/HttpResultsHelper.cs index 8b95be0a525f..b0e937383802 100644 --- a/src/Http/Http.Results/src/HttpResultsHelper.cs +++ b/src/Http/Http.Results/src/HttpResultsHelper.cs @@ -32,7 +32,6 @@ public static Task WriteResultAsJsonAsync( } jsonSerializerOptions ??= ResolveJsonOptions(httpContext).SerializerOptions; - jsonSerializerOptions.EnsureConfigured(); var jsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(TValue)); @@ -149,7 +148,7 @@ public static void ApplyProblemDetailsDefaultsIfNeeded(object? value, int? statu private static JsonOptions ResolveJsonOptions(HttpContext httpContext) { // Attempt to resolve options from DI then fallback to default options - return httpContext.RequestServices.GetService>()?.Value ?? new JsonOptions(); + return httpContext.RequestServices.GetService>()?.Value ?? JsonOptions.Default; } internal static partial class Log diff --git a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.WarningSuppressions.xml b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.WarningSuppressions.xml deleted file mode 100644 index 1531fc858fba..000000000000 --- a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.WarningSuppressions.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - ILLink - IL2026 - member - M:Microsoft.AspNetCore.Http.JsonSerializerExtensions.EnsureConfigured(System.Text.Json.JsonSerializerOptions) - This warning is left in the product so developers get an ILLink warning when trimming an app only when Microsoft.AspNetCore.EnsureJsonTrimmability=false. - - - diff --git a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj index 5695bf0df2b4..ef0ce7113472 100644 --- a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj +++ b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/Http/Http.Results/test/AcceptedAtRouteOfTResultTests.cs b/src/Http/Http.Results/test/AcceptedAtRouteOfTResultTests.cs index a4b085a27f9f..ab48d9ad14f7 100644 --- a/src/Http/Http.Results/test/AcceptedAtRouteOfTResultTests.cs +++ b/src/Http/Http.Results/test/AcceptedAtRouteOfTResultTests.cs @@ -220,6 +220,7 @@ private static IServiceProvider CreateServices(LinkGenerator linkGenerator) var services = new ServiceCollection(); services.AddLogging(); services.AddSingleton(linkGenerator); + services.AddDefaultHttpJsonOptions(); return services.BuildServiceProvider(); } } diff --git a/src/Http/Http.Results/test/AcceptedOfTResultTests.cs b/src/Http/Http.Results/test/AcceptedOfTResultTests.cs index 669eba8d529c..63e5934f5d13 100644 --- a/src/Http/Http.Results/test/AcceptedOfTResultTests.cs +++ b/src/Http/Http.Results/test/AcceptedOfTResultTests.cs @@ -144,6 +144,7 @@ private static IServiceProvider CreateServices() { var services = new ServiceCollection(); services.AddLogging(); + services.AddDefaultHttpJsonOptions(); return services.BuildServiceProvider(); } } diff --git a/src/Http/Http.Results/test/HttpResultsHelperTests.cs b/src/Http/Http.Results/test/HttpResultsHelperTests.cs index 0ebbdbe017a9..5b5d883d6de7 100644 --- a/src/Http/Http.Results/test/HttpResultsHelperTests.cs +++ b/src/Http/Http.Results/test/HttpResultsHelperTests.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Http.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -32,6 +33,10 @@ public async Task WriteResultAsJsonAsync_Works_ForValueTypes(bool useJsonContext { serializerOptions.AddContext(); } + else + { + serializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); + } // Act await HttpResultsHelper.WriteResultAsJsonAsync(httpContext, NullLogger.Instance, value, jsonSerializerOptions: serializerOptions); @@ -63,6 +68,10 @@ public async Task WriteResultAsJsonAsync_Works_ForReferenceTypes(bool useJsonCon { serializerOptions.AddContext(); } + else + { + serializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); + } // Act await HttpResultsHelper.WriteResultAsJsonAsync(httpContext, NullLogger.Instance, value, jsonSerializerOptions: serializerOptions); @@ -96,6 +105,10 @@ public async Task WriteResultAsJsonAsync_Works_ForChildTypes(bool useJsonContext { serializerOptions.AddContext(); } + else + { + serializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); + } // Act await HttpResultsHelper.WriteResultAsJsonAsync(httpContext, NullLogger.Instance, value, jsonSerializerOptions: serializerOptions); @@ -130,6 +143,10 @@ public async Task WriteResultAsJsonAsync_Works_UsingBaseType_ForChildTypes(bool { serializerOptions.AddContext(); } + else + { + serializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); + } // Act await HttpResultsHelper.WriteResultAsJsonAsync(httpContext, NullLogger.Instance, value, jsonSerializerOptions: serializerOptions); @@ -164,6 +181,10 @@ public async Task WriteResultAsJsonAsync_Works_UsingBaseType_ForChildTypes_WithJ { serializerOptions.AddContext(); } + else + { + serializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); + } // Act await HttpResultsHelper.WriteResultAsJsonAsync(httpContext, NullLogger.Instance, value, jsonSerializerOptions: serializerOptions); diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml deleted file mode 100644 index a675f48d5253..000000000000 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.WarningSuppressions.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - ILLink - IL2026 - member - M:Microsoft.AspNetCore.Http.JsonSerializerExtensions.EnsureConfigured(System.Text.Json.JsonSerializerOptions) - This warning is left in the product so developers get an ILLink warning when trimming an app only when Microsoft.AspNetCore.EnsureJsonTrimmability=false. - - - diff --git a/src/Http/Routing/src/RequestDelegateFilterPipelineBuilder.cs b/src/Http/Routing/src/RequestDelegateFilterPipelineBuilder.cs index 991d9aa0f337..af6d359a729c 100644 --- a/src/Http/Routing/src/RequestDelegateFilterPipelineBuilder.cs +++ b/src/Http/Routing/src/RequestDelegateFilterPipelineBuilder.cs @@ -19,15 +19,15 @@ public static RequestDelegate Create(RequestDelegate requestDelegate, RequestDel Debug.Assert(options.EndpointBuilder != null); var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; - var jsonOptions = serviceProvider?.GetService>()?.Value ?? new JsonOptions(); - var jsonSerializerOptions = jsonOptions.SerializerOptions; + var jsonSerializerOptions = serviceProvider?.GetService>()?.Value.SerializerOptions ?? JsonOptions.Default.SerializerOptions; + jsonSerializerOptions.MakeReadOnly(); var factoryContext = new EndpointFilterFactoryContext { MethodInfo = requestDelegate.Method, ApplicationServices = options.EndpointBuilder.ApplicationServices }; - var jsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetReadOnlyTypeInfo(typeof(object)); + var jsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(object)); EndpointFilterDelegate filteredInvocation = async (EndpointFilterInvocationContext context) => { diff --git a/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs b/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs index 74e5568ffd9d..b03a1342ed6e 100644 --- a/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs +++ b/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs @@ -37,6 +37,7 @@ public async Task MapPost_FromBodyWorksWithJsonPayload() .ConfigureServices(services => { services.AddRouting(); + services.AddDefaultHttpJsonOptions(); }) .Build(); diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs index bb34c6d64597..7c669a7dfca0 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs @@ -84,7 +84,7 @@ public DeveloperExceptionPageMiddlewareImpl( private static ExtensionsExceptionJsonContext CreateSerializationContext(JsonOptions? jsonOptions) { // Create context from configured options to get settings such as PropertyNamePolicy and DictionaryKeyPolicy. - jsonOptions ??= new JsonOptions(); + jsonOptions ??= JsonOptions.Default; return new ExtensionsExceptionJsonContext(new JsonSerializerOptions(jsonOptions.SerializerOptions)); } diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index a8178f76ca78..8a5b67a830e1 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -280,5 +280,6 @@ internal static void AddMvcCoreServices(IServiceCollection services) private static void ConfigureDefaultServices(IServiceCollection services) { services.AddRouting(); + services.AddDefaultHttpJsonOptions(); } } diff --git a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs index 7e49e66abf89..1cf6db74f5cf 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs @@ -21,8 +21,6 @@ public class SystemTextJsonOutputFormatter : TextOutputFormatter /// The . public SystemTextJsonOutputFormatter(JsonSerializerOptions jsonSerializerOptions) { - jsonSerializerOptions.EnsureConfigured(); - SerializerOptions = jsonSerializerOptions; SupportedEncodings.Add(Encoding.UTF8); @@ -34,6 +32,8 @@ public SystemTextJsonOutputFormatter(JsonSerializerOptions jsonSerializerOptions internal static SystemTextJsonOutputFormatter CreateFormatter(JsonOptions jsonOptions) { + jsonOptions.EnsureConfigured(); + var jsonSerializerOptions = jsonOptions.JsonSerializerOptions; if (jsonSerializerOptions.Encoder is null) diff --git a/src/Mvc/Mvc.Core/src/JsonOptions.cs b/src/Mvc/Mvc.Core/src/JsonOptions.cs index d3966142f027..397a2f980ee8 100644 --- a/src/Mvc/Mvc.Core/src/JsonOptions.cs +++ b/src/Mvc/Mvc.Core/src/JsonOptions.cs @@ -2,8 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text.Json; +using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Mvc; @@ -38,4 +40,19 @@ public class JsonOptions // This value is the same for model binding and Json.Net's serialization. MaxDepth = MvcOptions.DefaultMaxModelBindingRecursionDepth, }; + + internal JsonOptions EnsureConfigured() + { + if (!JsonSerializerOptions.IsReadOnly) + { + if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) + { + JsonSerializerOptions.InitializeForReflection(); + } + + JsonSerializerOptions.MakeReadOnly(); + } + + return this; + } } diff --git a/src/Shared/Json/JsonSerializerExtensions.cs b/src/Shared/Json/JsonSerializerExtensions.cs index 41c880a9081c..3fa26a0b9a23 100644 --- a/src/Shared/Json/JsonSerializerExtensions.cs +++ b/src/Shared/Json/JsonSerializerExtensions.cs @@ -5,7 +5,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; -using Microsoft.AspNetCore.Internal; namespace Microsoft.AspNetCore.Http; @@ -19,49 +18,19 @@ public static bool HasKnownPolymorphism(this JsonTypeInfo jsonTypeInfo) public static bool IsValid(this JsonTypeInfo jsonTypeInfo, [NotNullWhen(false)] Type? runtimeType) => runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.HasKnownPolymorphism(); - public static JsonTypeInfo GetReadOnlyTypeInfo(this JsonSerializerOptions options, Type type) - { - options.EnsureConfigured(); - - return options.GetTypeInfo(type); - } - - public static void EnsureConfigured(this JsonSerializerOptions options) - { - if (!options.IsReadOnly) - { - if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) - { -#pragma warning disable IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml -#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. - InitializeForReflection(options); -#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. -#pragma warning restore IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml - } - - options.MakeReadOnly(); - } - } + public static JsonTypeInfo GetRequiredTypeInfo(this JsonSerializerContext context, Type type) + => context.GetTypeInfo(type) ?? throw new InvalidOperationException($"Unable to obtain the JsonTypeInfo for type '{type.FullName}' from the context '{context.GetType().FullName}'."); [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] - private static void InitializeForReflection(JsonSerializerOptions options) + public static void InitializeForReflection(this JsonSerializerOptions options) { - // TODO: How can I avoid this lock? - lock (options) - { - if (!options.IsReadOnly) - { - _defaultJsonTypeInfoResolver ??= new DefaultJsonTypeInfoResolver(); + _defaultJsonTypeInfoResolver ??= new DefaultJsonTypeInfoResolver(); - options.TypeInfoResolver = options.TypeInfoResolver switch - { - null => _defaultJsonTypeInfoResolver, - _ => JsonTypeInfoResolver.Combine(options.TypeInfoResolver, _defaultJsonTypeInfoResolver), - }; - } - } + options.TypeInfoResolver = options.TypeInfoResolver switch + { + null => _defaultJsonTypeInfoResolver, + _ => JsonTypeInfoResolver.Combine(options.TypeInfoResolver, _defaultJsonTypeInfoResolver), + }; } - public static JsonTypeInfo GetRequiredTypeInfo(this JsonSerializerContext context, Type type) - => context.GetTypeInfo(type) ?? throw new InvalidOperationException($"Unable to obtain the JsonTypeInfo for type '{type.FullName}' from the context '{context.GetType().FullName}'."); } From 67faf3f42af301ac4eee396b3631b568aa27f4fa Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 6 Feb 2023 14:51:11 -0800 Subject: [PATCH 24/48] Clean up --- .../test/HttpRequestJsonExtensionsTests.cs | 1 - .../Http.Extensions/test/JsonOptionsTests.cs | 105 ++++++++++++++++++ .../test/JsonSerializerExtensionsTests.cs | 102 ----------------- .../Microsoft.AspNetCore.Http.Results.csproj | 1 - .../src/Microsoft.AspNetCore.Routing.csproj | 9 +- .../src/Properties/ILLink.Substitutions.xml | 8 -- .../SystemTextJsonOutputFormatterTest.cs | 3 +- src/Mvc/Mvc.Core/test/JsonOptionsTest.cs | 104 +++++++++++++++++ 8 files changed, 213 insertions(+), 120 deletions(-) create mode 100644 src/Http/Http.Extensions/test/JsonOptionsTests.cs delete mode 100644 src/Http/Http.Extensions/test/JsonSerializerExtensionsTests.cs delete mode 100644 src/Http/Routing/src/Properties/ILLink.Substitutions.xml create mode 100644 src/Mvc/Mvc.Core/test/JsonOptionsTest.cs diff --git a/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs b/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs index e761e95ee70b..4d63107214e9 100644 --- a/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs +++ b/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs @@ -4,7 +4,6 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization.Metadata; -using Microsoft.Extensions.DependencyInjection; #nullable enable diff --git a/src/Http/Http.Extensions/test/JsonOptionsTests.cs b/src/Http/Http.Extensions/test/JsonOptionsTests.cs new file mode 100644 index 000000000000..e3c446c501c2 --- /dev/null +++ b/src/Http/Http.Extensions/test/JsonOptionsTests.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.AspNetCore.Testing; +using Microsoft.DotNet.RemoteExecutor; + +namespace Microsoft.AspNetCore.Http.Extensions.Tests; + +public partial class JsonOptionsTests +{ + [ConditionalFact] + [RemoteExecutionSupported] + public void Configure_ThrowsForNullTypeInfoResolver_WhenEnsureJsonTrimmabilityTrue() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var options = new JsonSerializerOptions(); + + // Act & Assert + Assert.Throws(() => new JsonOptions().EnsureConfigured()); + }, options); + } + + [ConditionalFact] + [RemoteExecutionSupported] + public void Configure_Works_WhenEnsureJsonTrimmabilityTrue() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var options = new JsonOptions(); + options.SerializerOptions.AddContext(); + + // Act + var jsonOptions = options.EnsureConfigured(); + + // Assert + var serializerOptions = options.SerializerOptions; + Assert.NotNull(serializerOptions.TypeInfoResolver); + Assert.IsType(serializerOptions.TypeInfoResolver); + Assert.True(serializerOptions.IsReadOnly); + }, options); + } + + [ConditionalFact] + [RemoteExecutionSupported] + public void DefaultSerializerOptions_Works_WhenEnsureJsonTrimmabilityFalse() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Act + var options = new JsonOptions().EnsureConfigured(); + + // Assert + var serializerOptions = options.SerializerOptions; + Assert.NotNull(serializerOptions.TypeInfoResolver); + Assert.IsType(serializerOptions.TypeInfoResolver); + Assert.True(serializerOptions.IsReadOnly); + }, options); + } + + [ConditionalFact] + [RemoteExecutionSupported] + public void DefaultSerializerOptions_Combines_WhenEnsureJsonTrimmabilityFalse() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var options = new JsonOptions(); + options.SerializerOptions.AddContext(); + + // Act + var jsonOptions = options.EnsureConfigured(); + + // Assert + var serializerOptions = options.SerializerOptions; + Assert.NotNull(serializerOptions.TypeInfoResolver); + Assert.IsNotType(serializerOptions.TypeInfoResolver); + Assert.IsNotType(serializerOptions.TypeInfoResolver); + Assert.NotNull(serializerOptions.TypeInfoResolver.GetTypeInfo(typeof(string), serializerOptions)); + Assert.True(serializerOptions.IsReadOnly); + }, options); + } + + [JsonSerializable(typeof(object))] + private partial class JsonSerializerExtensionsTestsContext : JsonSerializerContext + { } +} diff --git a/src/Http/Http.Extensions/test/JsonSerializerExtensionsTests.cs b/src/Http/Http.Extensions/test/JsonSerializerExtensionsTests.cs deleted file mode 100644 index af75310a7286..000000000000 --- a/src/Http/Http.Extensions/test/JsonSerializerExtensionsTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; -using Microsoft.AspNetCore.Testing; -using Microsoft.DotNet.RemoteExecutor; - -namespace Microsoft.AspNetCore.Http.Extensions.Tests; - -public partial class JsonSerializerExtensionsTests -{ - //[ConditionalFact] - //[RemoteExecutionSupported] - //public void Configure_ThrowsForNullTypeInfoResolver_WhenEnsureJsonTrimmabilityTrue() - //{ - // var options = new RemoteInvokeOptions(); - // options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); - - // using var remoteHandle = RemoteExecutor.Invoke(static () => - // { - // // Arrange - // var options = new JsonSerializerOptions(); - - // // Act & Assert - // Assert.Throws(() => JsonSerializerExtensions.EnsureConfigured(options)); - // }, options); - //} - - //[ConditionalFact] - //[RemoteExecutionSupported] - //public void Configure_Works_WhenEnsureJsonTrimmabilityTrue() - //{ - // var options = new RemoteInvokeOptions(); - // options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); - - // using var remoteHandle = RemoteExecutor.Invoke(static () => - // { - // // Arrange - // var options = new JsonSerializerOptions() { TypeInfoResolver = JsonSerializerExtensionsTestsContext.Default }; - - // // Act - // JsonSerializerExtensions.EnsureConfigured(options); - - // // Assert - // Assert.NotNull(options.TypeInfoResolver); - // Assert.IsType(options.TypeInfoResolver); - // Assert.True(options.IsReadOnly); - // }, options); - //} - - //[ConditionalFact] - //[RemoteExecutionSupported] - //public void DefaultSerializerOptions_Works_WhenEnsureJsonTrimmabilityFalse() - //{ - // var options = new RemoteInvokeOptions(); - // options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); - - // using var remoteHandle = RemoteExecutor.Invoke(static () => - // { - // // Arrange - // var options = new JsonSerializerOptions(); - - // // Act - // JsonSerializerExtensions.EnsureConfigured(options); - - // // Assert - // Assert.NotNull(options.TypeInfoResolver); - // Assert.IsType(options.TypeInfoResolver); - // Assert.True(options.IsReadOnly); - // }, options); - //} - - //[ConditionalFact] - //[RemoteExecutionSupported] - //public void DefaultSerializerOptions_Combines_WhenEnsureJsonTrimmabilityFalse() - //{ - // var options = new RemoteInvokeOptions(); - // options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); - - // using var remoteHandle = RemoteExecutor.Invoke(static () => - // { - // // Arrange - // var options = new JsonSerializerOptions() { TypeInfoResolver = JsonSerializerExtensionsTestsContext.Default }; - - // // Act - // JsonSerializerExtensions.EnsureConfigured(options); - - // // Assert - // Assert.NotNull(options.TypeInfoResolver); - // Assert.IsNotType(options.TypeInfoResolver); - // Assert.IsNotType(options.TypeInfoResolver); - // Assert.NotNull(options.TypeInfoResolver.GetTypeInfo(typeof(string), options)); - // Assert.True(options.IsReadOnly); - // }, options); - //} - - [JsonSerializable(typeof(object))] - private partial class JsonSerializerExtensionsTestsContext : JsonSerializerContext - { } -} diff --git a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj index ef0ce7113472..bf5308c62aad 100644 --- a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj +++ b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj @@ -20,7 +20,6 @@ - diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index e3528ffa27fd..96eea7c605c1 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -31,16 +31,11 @@ - - - + + - - - - diff --git a/src/Http/Routing/src/Properties/ILLink.Substitutions.xml b/src/Http/Routing/src/Properties/ILLink.Substitutions.xml deleted file mode 100644 index 3beb4a06f235..000000000000 --- a/src/Http/Routing/src/Properties/ILLink.Substitutions.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs index 4cb13706a8ce..d2741442826e 100644 --- a/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs +++ b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs @@ -209,7 +209,8 @@ public void CreateFormatter_DoesNotThrow_WhenTypeResolverIsNull() Assert.IsType< SystemTextJsonOutputFormatter>(SystemTextJsonOutputFormatter.CreateFormatter(jsonOptions)); } - [Fact] + [ConditionalFact] + [RemoteExecutionSupported] public void CreateFormatter_Throws_WhenTypeResolverIsNull_AndEnsureJsonTrimmabilityTrue() { var options = new RemoteInvokeOptions(); diff --git a/src/Mvc/Mvc.Core/test/JsonOptionsTest.cs b/src/Mvc/Mvc.Core/test/JsonOptionsTest.cs new file mode 100644 index 000000000000..d53ee688700d --- /dev/null +++ b/src/Mvc/Mvc.Core/test/JsonOptionsTest.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using Microsoft.AspNetCore.Testing; +using Microsoft.DotNet.RemoteExecutor; + +namespace Microsoft.AspNetCore.Mvc; + +public partial class JsonOptionsTest +{ + [ConditionalFact] + [RemoteExecutionSupported] + public void Configure_ThrowsForNullTypeInfoResolver_WhenEnsureJsonTrimmabilityTrue() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var options = new JsonSerializerOptions(); + + // Act & Assert + Assert.Throws(() => new JsonOptions().EnsureConfigured()); + }, options); + } + + [ConditionalFact] + [RemoteExecutionSupported] + public void Configure_Works_WhenEnsureJsonTrimmabilityTrue() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var options = new JsonOptions(); + options.JsonSerializerOptions.AddContext(); + + // Act + var jsonOptions = options.EnsureConfigured(); + + // Assert + var serializerOptions = options.JsonSerializerOptions; + Assert.NotNull(serializerOptions.TypeInfoResolver); + Assert.IsType(serializerOptions.TypeInfoResolver); + Assert.True(serializerOptions.IsReadOnly); + }, options); + } + + [ConditionalFact] + [RemoteExecutionSupported] + public void DefaultSerializerOptions_Works_WhenEnsureJsonTrimmabilityFalse() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Act + var options = new JsonOptions().EnsureConfigured(); + + // Assert + var serializerOptions = options.JsonSerializerOptions; + Assert.NotNull(serializerOptions.TypeInfoResolver); + Assert.IsType(serializerOptions.TypeInfoResolver); + Assert.True(serializerOptions.IsReadOnly); + }, options); + } + + [ConditionalFact] + [RemoteExecutionSupported] + public void DefaultSerializerOptions_Combines_WhenEnsureJsonTrimmabilityFalse() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var options = new JsonOptions(); + options.JsonSerializerOptions.AddContext(); + + // Act + var jsonOptions = options.EnsureConfigured(); + + // Assert + var serializerOptions = options.JsonSerializerOptions; + Assert.NotNull(serializerOptions.TypeInfoResolver); + Assert.IsNotType(serializerOptions.TypeInfoResolver); + Assert.IsNotType(serializerOptions.TypeInfoResolver); + Assert.NotNull(serializerOptions.TypeInfoResolver.GetTypeInfo(typeof(string), serializerOptions)); + Assert.True(serializerOptions.IsReadOnly); + }, options); + } + + [JsonSerializable(typeof(object))] + private partial class JsonSerializerExtensionsTestsContext : JsonSerializerContext + { } +} From ede0a7e301f4777ef1e7bc8b7d9a3cc196d8548d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 7 Feb 2023 14:19:52 -0800 Subject: [PATCH 25/48] Fixing unit tests --- .../src/ProblemDetailsServiceCollectionExtensions.cs | 3 +++ .../Diagnostics/test/UnitTests/StatusCodeMiddlewareTest.cs | 4 ++-- src/Security/Authentication/test/JwtBearerTests.cs | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs b/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs index f76540a67838..1b38c7fdfc81 100644 --- a/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs +++ b/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs @@ -37,6 +37,9 @@ public static IServiceCollection AddProblemDetails( { ArgumentNullException.ThrowIfNull(services); + // Adding default HttpJsonOptions. + services.AddDefaultHttpJsonOptions(); + // Adding default services; services.TryAddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); diff --git a/src/Middleware/Diagnostics/test/UnitTests/StatusCodeMiddlewareTest.cs b/src/Middleware/Diagnostics/test/UnitTests/StatusCodeMiddlewareTest.cs index 52214d97ff90..d128d72d2b3e 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/StatusCodeMiddlewareTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/StatusCodeMiddlewareTest.cs @@ -321,7 +321,7 @@ public async Task SkipStatusCodePages_SupportsSkipIfUsedBeforeRouting() .ConfigureWebHost(builder => { builder.UseTestServer() - .ConfigureServices(services => services.AddRouting()) + .ConfigureServices(services => services.AddRouting().AddDefaultHttpJsonOptions()) .Configure(app => { app.UseStatusCodePagesWithReExecute("/status"); @@ -359,7 +359,7 @@ public async Task SkipStatusCodePages_WorksIfUsedBeforeRouting() .ConfigureWebHost(builder => { builder.UseTestServer() - .ConfigureServices(services => services.AddRouting()) + .ConfigureServices(services => services.AddRouting().AddDefaultHttpJsonOptions()) .Configure(app => { app.UseStatusCodePagesWithReExecute("/status"); diff --git a/src/Security/Authentication/test/JwtBearerTests.cs b/src/Security/Authentication/test/JwtBearerTests.cs index 6f2160830053..1fa1cf2a93cd 100755 --- a/src/Security/Authentication/test/JwtBearerTests.cs +++ b/src/Security/Authentication/test/JwtBearerTests.cs @@ -1174,7 +1174,7 @@ private static async Task CreateHost(Action options = n { var authenticationResult = await context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); await context.Response.WriteAsJsonAsync( - new { Expires = authenticationResult.Properties?.ExpiresUtc, Issued = authenticationResult.Properties?.IssuedUtc }); + new { Expires = authenticationResult.Properties?.ExpiresUtc, Issued = authenticationResult.Properties?.IssuedUtc }, Http.Json.JsonOptions.Default.SerializerOptions); } else { From a7ae048374f36cfaa03cbe7005a5942f97bf559a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 7 Feb 2023 16:11:47 -0800 Subject: [PATCH 26/48] Fix unit test --- .../src/ProblemDetailsServiceCollectionExtensions.cs | 6 +++--- .../test/ProblemDetailsServiceCollectionExtensionsTest.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs b/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs index 1b38c7fdfc81..798858715d5c 100644 --- a/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs +++ b/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs @@ -37,9 +37,6 @@ public static IServiceCollection AddProblemDetails( { ArgumentNullException.ThrowIfNull(services); - // Adding default HttpJsonOptions. - services.AddDefaultHttpJsonOptions(); - // Adding default services; services.TryAddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); @@ -50,6 +47,9 @@ public static IServiceCollection AddProblemDetails( services.Configure(configure); } + // Adding default HttpJsonOptions. + services.AddDefaultHttpJsonOptions(); + return services; } } diff --git a/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs index 5199842de795..eee344086cbb 100644 --- a/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs @@ -160,7 +160,7 @@ public void AddProblemDetails_DoesNotCombineProblemDetailsContext_WhenNullTypeIn var jsonOptions = services.GetService>(); Assert.NotNull(jsonOptions.Value); - Assert.Null(jsonOptions.Value.SerializerOptions.TypeInfoResolver); + Assert.IsType(jsonOptions.Value.SerializerOptions.TypeInfoResolver); } [Fact] From 422cb93baf0325f6a3d96cd13c33527ee5b7fafe Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 7 Feb 2023 17:21:39 -0800 Subject: [PATCH 27/48] Trying fix SignalR --- .../server/SignalR/src/SignalRDependencyInjectionExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SignalR/server/SignalR/src/SignalRDependencyInjectionExtensions.cs b/src/SignalR/server/SignalR/src/SignalRDependencyInjectionExtensions.cs index 9b2eded595d6..ae781cde62ac 100644 --- a/src/SignalR/server/SignalR/src/SignalRDependencyInjectionExtensions.cs +++ b/src/SignalR/server/SignalR/src/SignalRDependencyInjectionExtensions.cs @@ -43,6 +43,7 @@ public static ISignalRServerBuilder AddSignalR(this IServiceCollection services) services.Configure(o => o.KeepAliveInterval = TimeSpan.Zero); services.TryAddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton, HubOptionsSetup>()); + services.AddDefaultHttpJsonOptions(); return services.AddSignalRCore(); } From 8edd3b7a483e748e0c674cc8a7933536b03e0f0e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 7 Feb 2023 22:27:10 -0800 Subject: [PATCH 28/48] Revert "Trying fix SignalR" This reverts commit 422cb93baf0325f6a3d96cd13c33527ee5b7fafe. --- .../server/SignalR/src/SignalRDependencyInjectionExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/SignalR/server/SignalR/src/SignalRDependencyInjectionExtensions.cs b/src/SignalR/server/SignalR/src/SignalRDependencyInjectionExtensions.cs index ae781cde62ac..9b2eded595d6 100644 --- a/src/SignalR/server/SignalR/src/SignalRDependencyInjectionExtensions.cs +++ b/src/SignalR/server/SignalR/src/SignalRDependencyInjectionExtensions.cs @@ -43,7 +43,6 @@ public static ISignalRServerBuilder AddSignalR(this IServiceCollection services) services.Configure(o => o.KeepAliveInterval = TimeSpan.Zero); services.TryAddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton, HubOptionsSetup>()); - services.AddDefaultHttpJsonOptions(); return services.AddSignalRCore(); } From cde347e3669d9c203c7e3fc29097111f320520f6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 7 Feb 2023 22:48:20 -0800 Subject: [PATCH 29/48] Update Startup.cs --- src/SignalR/clients/ts/FunctionalTests/Startup.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/SignalR/clients/ts/FunctionalTests/Startup.cs b/src/SignalR/clients/ts/FunctionalTests/Startup.cs index 67eb2878ade3..1c3ee3dfcd00 100644 --- a/src/SignalR/clients/ts/FunctionalTests/Startup.cs +++ b/src/SignalR/clients/ts/FunctionalTests/Startup.cs @@ -91,6 +91,8 @@ public void ConfigureServices(IServiceCollection services) } }; }); + + services.AddDefaultHttpJsonOptions(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger logger) From a18c01b95a55ef0d2d2c59f772e31ec21537de32 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 8 Feb 2023 15:51:00 -0800 Subject: [PATCH 30/48] Changing to a IOptionsFactory --- src/DefaultBuilder/src/WebHost.cs | 3 -- .../src/DefaultHttpJsonOptionsSetup.cs | 15 ------- .../src/HttpJsonOptionsFactory.cs | 44 +++++++++++++++++++ .../src/HttpJsonServiceExtensions.cs | 2 +- ...oblemDetailsServiceCollectionExtensions.cs | 6 +-- .../Http.Results/src/JsonHttpResultOfT.cs | 2 +- .../RoutingServiceCollectionExtensions.cs | 3 ++ .../UnitTests/StatusCodeMiddlewareTest.cs | 4 +- .../MvcCoreServiceCollectionExtensions.cs | 1 - .../clients/ts/FunctionalTests/Startup.cs | 2 - 10 files changed, 54 insertions(+), 28 deletions(-) delete mode 100644 src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs create mode 100644 src/Http/Http.Extensions/src/HttpJsonOptionsFactory.cs diff --git a/src/DefaultBuilder/src/WebHost.cs b/src/DefaultBuilder/src/WebHost.cs index 331757777e98..4a49c3ef8e3c 100644 --- a/src/DefaultBuilder/src/WebHost.cs +++ b/src/DefaultBuilder/src/WebHost.cs @@ -258,9 +258,6 @@ internal static void UseKestrel(IWebHostBuilder builder) services.AddTransient, ForwardedHeadersOptionsSetup>(); services.AddRouting(); - - // JsonOptions - services.AddDefaultHttpJsonOptions(); }); } diff --git a/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs b/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs deleted file mode 100644 index 05c60913d375..000000000000 --- a/src/Http/Http.Extensions/src/DefaultHttpJsonOptionsSetup.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Http.Json; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Http; - -internal sealed class DefaultHttpJsonOptionsSetup : IPostConfigureOptions -{ - public void PostConfigure(string? name, JsonOptions options) - { - options.EnsureConfigured(markAsReadOnly: false); - } -} diff --git a/src/Http/Http.Extensions/src/HttpJsonOptionsFactory.cs b/src/Http/Http.Extensions/src/HttpJsonOptionsFactory.cs new file mode 100644 index 000000000000..2b4dfacea2ac --- /dev/null +++ b/src/Http/Http.Extensions/src/HttpJsonOptionsFactory.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.Json; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Http; + +internal sealed class HttpJsonOptionsFactory : IOptionsFactory +{ + private readonly OptionsFactory _innerOptionsFactory; + + /// + /// Initializes a new instance with the specified options configurations. + /// + /// The configuration actions to run. + /// The initialization actions to run. + public HttpJsonOptionsFactory(IEnumerable> setups, IEnumerable> postConfigures) + : this(setups, postConfigures, validations: Array.Empty>()) + { } + + /// + /// Initializes a new instance with the specified options configurations. + /// + /// The configuration actions to run. + /// The initialization actions to run. + /// The validations to run. + public HttpJsonOptionsFactory(IEnumerable> setups, IEnumerable> postConfigures, IEnumerable> validations) + { + _innerOptionsFactory = new OptionsFactory(setups, postConfigures, validations); + } + + public JsonOptions Create(string name) + { + var options = _innerOptionsFactory.Create(name); + + // After the options is completed created + // we need to ensure we have it configured for + // reflection (if needed) and marked as read-only + options.EnsureConfigured(); + + return options; + } +} diff --git a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs index 184681ce2649..f001ca80785a 100644 --- a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs @@ -44,7 +44,7 @@ public static IServiceCollection AddDefaultHttpJsonOptions(this IServiceCollecti ArgumentNullException.ThrowIfNull(services); services.AddOptions(); - services.TryAddEnumerable(ServiceDescriptor.Singleton, DefaultHttpJsonOptionsSetup>()); + services.TryAddEnumerable(ServiceDescriptor.Singleton, HttpJsonOptionsFactory>()); return services; } diff --git a/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs b/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs index 798858715d5c..1b38c7fdfc81 100644 --- a/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs +++ b/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs @@ -37,6 +37,9 @@ public static IServiceCollection AddProblemDetails( { ArgumentNullException.ThrowIfNull(services); + // Adding default HttpJsonOptions. + services.AddDefaultHttpJsonOptions(); + // Adding default services; services.TryAddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); @@ -47,9 +50,6 @@ public static IServiceCollection AddProblemDetails( services.Configure(configure); } - // Adding default HttpJsonOptions. - services.AddDefaultHttpJsonOptions(); - return services; } } diff --git a/src/Http/Http.Results/src/JsonHttpResultOfT.cs b/src/Http/Http.Results/src/JsonHttpResultOfT.cs index 4841e675ecfd..a9909d19f3a1 100644 --- a/src/Http/Http.Results/src/JsonHttpResultOfT.cs +++ b/src/Http/Http.Results/src/JsonHttpResultOfT.cs @@ -38,7 +38,7 @@ internal JsonHttpResult(TValue? value, JsonSerializerOptions? jsonSerializerOpti if (jsonSerializerOptions is not null && !jsonSerializerOptions.IsReadOnly) { - jsonSerializerOptions.TypeInfoResolver ??= new DefaultJsonTypeInfoResolver(); + jsonSerializerOptions.InitializeForReflection(); } JsonSerializerOptions = jsonSerializerOptions; diff --git a/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs index 6c912748bca5..fc6cbeb76f86 100644 --- a/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs +++ b/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs @@ -98,6 +98,9 @@ public static IServiceCollection AddRouting(this IServiceCollection services) // Set RouteHandlerOptions.ThrowOnBadRequest in development services.TryAddEnumerable(ServiceDescriptor.Transient, ConfigureRouteHandlerOptions>()); + // Set defaults for Http.JsonOptions required for RDF + services.AddDefaultHttpJsonOptions(); + return services; } diff --git a/src/Middleware/Diagnostics/test/UnitTests/StatusCodeMiddlewareTest.cs b/src/Middleware/Diagnostics/test/UnitTests/StatusCodeMiddlewareTest.cs index d128d72d2b3e..52214d97ff90 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/StatusCodeMiddlewareTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/StatusCodeMiddlewareTest.cs @@ -321,7 +321,7 @@ public async Task SkipStatusCodePages_SupportsSkipIfUsedBeforeRouting() .ConfigureWebHost(builder => { builder.UseTestServer() - .ConfigureServices(services => services.AddRouting().AddDefaultHttpJsonOptions()) + .ConfigureServices(services => services.AddRouting()) .Configure(app => { app.UseStatusCodePagesWithReExecute("/status"); @@ -359,7 +359,7 @@ public async Task SkipStatusCodePages_WorksIfUsedBeforeRouting() .ConfigureWebHost(builder => { builder.UseTestServer() - .ConfigureServices(services => services.AddRouting().AddDefaultHttpJsonOptions()) + .ConfigureServices(services => services.AddRouting()) .Configure(app => { app.UseStatusCodePagesWithReExecute("/status"); diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 8a5b67a830e1..a8178f76ca78 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -280,6 +280,5 @@ internal static void AddMvcCoreServices(IServiceCollection services) private static void ConfigureDefaultServices(IServiceCollection services) { services.AddRouting(); - services.AddDefaultHttpJsonOptions(); } } diff --git a/src/SignalR/clients/ts/FunctionalTests/Startup.cs b/src/SignalR/clients/ts/FunctionalTests/Startup.cs index 1c3ee3dfcd00..67eb2878ade3 100644 --- a/src/SignalR/clients/ts/FunctionalTests/Startup.cs +++ b/src/SignalR/clients/ts/FunctionalTests/Startup.cs @@ -91,8 +91,6 @@ public void ConfigureServices(IServiceCollection services) } }; }); - - services.AddDefaultHttpJsonOptions(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger logger) From d45eea537df214d7130558ba21b86e93eb83a2e8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 8 Feb 2023 15:54:30 -0800 Subject: [PATCH 31/48] Move to singleton --- src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs index f001ca80785a..d27929b15795 100644 --- a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs @@ -44,7 +44,7 @@ public static IServiceCollection AddDefaultHttpJsonOptions(this IServiceCollecti ArgumentNullException.ThrowIfNull(services); services.AddOptions(); - services.TryAddEnumerable(ServiceDescriptor.Singleton, HttpJsonOptionsFactory>()); + services.TryAddSingleton, HttpJsonOptionsFactory>(); return services; } From 298f9e5a42ae01b58c3fd8a9c62b3876ed07b92f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 9 Feb 2023 23:09:26 -0800 Subject: [PATCH 32/48] Trying another approach --- src/DefaultBuilder/src/WebHost.cs | 1 + .../src/HttpJsonOptionsFactory.cs | 2 +- .../src/HttpJsonServiceExtensions.cs | 2 +- .../src/HttpRequestJsonExtensions.cs | 98 +++------- .../src/HttpResponseJsonExtensions.cs | 145 ++++----------- src/Http/Http.Extensions/src/JsonOptions.cs | 26 +-- ...re.Http.Extensions.WarningSuppressions.xml | 4 +- ...icrosoft.AspNetCore.Http.Extensions.csproj | 2 +- .../src/RequestDelegateFactory.cs | 35 ++-- .../Http.Extensions/test/JsonOptionsTests.cs | 172 +++++++++--------- .../Http.Results/src/JsonHttpResultOfT.cs | 2 +- .../RoutingServiceCollectionExtensions.cs | 3 - .../RequestDelegateFilterPipelineBuilder.cs | 5 +- .../SystemTextJsonOutputFormatter.cs | 3 +- src/Mvc/Mvc.Core/src/JsonOptions.cs | 17 -- .../src/Microsoft.AspNetCore.Mvc.Core.csproj | 2 +- src/Shared/Json/JsonSerializerExtensions.cs | 16 -- .../Json/JsonSerializerOptionsExtensions.cs | 49 +++++ .../RouteHandlers/ExecuteHandlerHelper.cs | 5 +- 19 files changed, 234 insertions(+), 355 deletions(-) create mode 100644 src/Shared/Json/JsonSerializerOptionsExtensions.cs diff --git a/src/DefaultBuilder/src/WebHost.cs b/src/DefaultBuilder/src/WebHost.cs index 4a49c3ef8e3c..ba8900982e9c 100644 --- a/src/DefaultBuilder/src/WebHost.cs +++ b/src/DefaultBuilder/src/WebHost.cs @@ -256,6 +256,7 @@ internal static void UseKestrel(IWebHostBuilder builder) services.AddTransient(); services.AddTransient(); services.AddTransient, ForwardedHeadersOptionsSetup>(); + services.AddDefaultHttpJsonOptions(); services.AddRouting(); }); diff --git a/src/Http/Http.Extensions/src/HttpJsonOptionsFactory.cs b/src/Http/Http.Extensions/src/HttpJsonOptionsFactory.cs index 2b4dfacea2ac..2149dc42b314 100644 --- a/src/Http/Http.Extensions/src/HttpJsonOptionsFactory.cs +++ b/src/Http/Http.Extensions/src/HttpJsonOptionsFactory.cs @@ -37,7 +37,7 @@ public JsonOptions Create(string name) // After the options is completed created // we need to ensure we have it configured for // reflection (if needed) and marked as read-only - options.EnsureConfigured(); + options.SerializerOptions.EnsureConfigured(markAsReadOnly: true); return options; } diff --git a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs index d27929b15795..8b05b5db4d7d 100644 --- a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs @@ -44,7 +44,7 @@ public static IServiceCollection AddDefaultHttpJsonOptions(this IServiceCollecti ArgumentNullException.ThrowIfNull(services); services.AddOptions(); - services.TryAddSingleton, HttpJsonOptionsFactory>(); + services.TryAddTransient, HttpJsonOptionsFactory>(); return services; } diff --git a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs index 9333635a802e..54f758da87ee 100644 --- a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs @@ -19,10 +19,6 @@ namespace Microsoft.AspNetCore.Http; /// public static class HttpRequestJsonExtensions { - private const string RequiresUnreferencedCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed. " + - "Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved."; - private const string RequiresDynamicCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed and need runtime code generation. " + - "Use the overload that takes a JsonTypeInfo or JsonSerializerContext for native AOT applications."; /// /// Read JSON from the request and deserialize to the specified type. @@ -51,36 +47,17 @@ public static class HttpRequestJsonExtensions /// The serializer options to use when deserializing the content. /// A used to cancel the operation. /// The task object representing the asynchronous operation. - [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] - [RequiresDynamicCode(RequiresDynamicCodeMessage)] - public static async ValueTask ReadFromJsonAsync( + public static ValueTask ReadFromJsonAsync( this HttpRequest request, JsonSerializerOptions? options, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); - if (!request.HasJsonContentType(out var charset)) - { - ThrowContentTypeError(request); - } - options ??= ResolveSerializerOptions(request.HttpContext); + options.EnsureConfigured(); - var encoding = GetEncodingFromCharset(charset); - var (inputStream, usesTranscodingStream) = GetInputStream(request.HttpContext, encoding); - - try - { - return await JsonSerializer.DeserializeAsync(inputStream, options, cancellationToken); - } - finally - { - if (usesTranscodingStream) - { - await inputStream.DisposeAsync(); - } - } + return ReadFromJsonAsync(request, (JsonTypeInfo)options.GetTypeInfo(typeof(TValue)), cancellationToken); } /// @@ -122,41 +99,22 @@ public static class HttpRequestJsonExtensions } /// - /// Read JSON from the request and deserialize to object type. + /// Read JSON from the request and deserialize to the specified type. /// If the request's content-type is not a known JSON type then an error will be thrown. /// /// The request to read from. - /// Metadata about the type to convert. + /// The type of object to read. /// A used to cancel the operation. - /// The deserialized value. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters - public static async ValueTask ReadFromJsonAsync( -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + /// The task object representing the asynchronous operation. + public static ValueTask ReadFromJsonAsync( this HttpRequest request, - JsonTypeInfo jsonTypeInfo, + Type type, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); - if (!request.HasJsonContentType(out var charset)) - { - ThrowContentTypeError(request); - } - - var encoding = GetEncodingFromCharset(charset); - var (inputStream, usesTranscodingStream) = GetInputStream(request.HttpContext, encoding); - - try - { - return await JsonSerializer.DeserializeAsync(inputStream, jsonTypeInfo, cancellationToken); - } - finally - { - if (usesTranscodingStream) - { - await inputStream.DisposeAsync(); - } - } + var options = ResolveSerializerOptions(request.HttpContext); + return request.ReadFromJsonAsync(jsonTypeInfo: options.GetTypeInfo(type), cancellationToken); } /// @@ -165,17 +123,22 @@ public static class HttpRequestJsonExtensions /// /// The request to read from. /// The type of object to read. + /// The serializer options use when deserializing the content. /// A used to cancel the operation. /// The task object representing the asynchronous operation. public static ValueTask ReadFromJsonAsync( this HttpRequest request, Type type, + JsonSerializerOptions? options, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(type); - var options = ResolveSerializerOptions(request.HttpContext); - return request.ReadFromJsonAsync(jsonTypeInfo: options.GetTypeInfo(type), cancellationToken); + options ??= ResolveSerializerOptions(request.HttpContext); + options.EnsureConfigured(); + + return ReadFromJsonAsync(request, options.GetTypeInfo(type), cancellationToken); } /// @@ -184,33 +147,32 @@ public static class HttpRequestJsonExtensions /// /// The request to read from. /// The type of object to read. - /// The serializer options use when deserializing the content. + /// A metadata provider for serializable types. /// A used to cancel the operation. - /// The task object representing the asynchronous operation. - [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] - [RequiresDynamicCode(RequiresDynamicCodeMessage)] + /// The deserialized value. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static async ValueTask ReadFromJsonAsync( +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters this HttpRequest request, Type type, - JsonSerializerOptions? options, + JsonSerializerContext context, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(type); + ArgumentNullException.ThrowIfNull(context); if (!request.HasJsonContentType(out var charset)) { ThrowContentTypeError(request); } - options ??= ResolveSerializerOptions(request.HttpContext); - var encoding = GetEncodingFromCharset(charset); var (inputStream, usesTranscodingStream) = GetInputStream(request.HttpContext, encoding); try { - return await JsonSerializer.DeserializeAsync(inputStream, type, options, cancellationToken); + return await JsonSerializer.DeserializeAsync(inputStream, type, context, cancellationToken); } finally { @@ -222,25 +184,21 @@ public static class HttpRequestJsonExtensions } /// - /// Read JSON from the request and deserialize to the specified type. + /// Read JSON from the request and deserialize to object type. /// If the request's content-type is not a known JSON type then an error will be thrown. /// /// The request to read from. - /// The type of object to read. - /// A metadata provider for serializable types. + /// Metadata about the type to convert. /// A used to cancel the operation. /// The deserialized value. #pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static async ValueTask ReadFromJsonAsync( #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters this HttpRequest request, - Type type, - JsonSerializerContext context, + JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); - ArgumentNullException.ThrowIfNull(type); - ArgumentNullException.ThrowIfNull(context); if (!request.HasJsonContentType(out var charset)) { @@ -252,7 +210,7 @@ public static class HttpRequestJsonExtensions try { - return await JsonSerializer.DeserializeAsync(inputStream, type, context, cancellationToken); + return await JsonSerializer.DeserializeAsync(inputStream, jsonTypeInfo, cancellationToken); } finally { diff --git a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs index 218ceb9959d3..9d4cd9c0942c 100644 --- a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -16,11 +15,6 @@ namespace Microsoft.AspNetCore.Http; /// public static partial class HttpResponseJsonExtensions { - private const string RequiresUnreferencedCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed. " + - "Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved."; - private const string RequiresDynamicCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed and need runtime code generation. " + - "Use the overload that takes a JsonTypeInfo or JsonSerializerContext for native AOT applications."; - /// /// Write the specified value as JSON to the response body. The response content-type will be set to /// application/json; charset=utf-8. @@ -35,10 +29,7 @@ public static Task WriteAsJsonAsync( TValue value, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(response); - - var options = ResolveSerializerOptions(response.HttpContext); - return response.WriteAsJsonAsync(value, jsonTypeInfo: (JsonTypeInfo)options.GetTypeInfo(typeof(TValue)), contentType: null, cancellationToken); + return response.WriteAsJsonAsync(value, options: null, contentType: null, cancellationToken); } /// @@ -51,8 +42,6 @@ public static Task WriteAsJsonAsync( /// The serializer options to use when serializing the value. /// A used to cancel the operation. /// The task object representing the asynchronous operation. - [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] - [RequiresDynamicCode(RequiresDynamicCodeMessage)] public static Task WriteAsJsonAsync( this HttpResponse response, TValue value, @@ -73,8 +62,6 @@ public static Task WriteAsJsonAsync( /// The content-type to set on the response. /// A used to cancel the operation. /// The task object representing the asynchronous operation. - [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] - [RequiresDynamicCode(RequiresDynamicCodeMessage)] public static Task WriteAsJsonAsync( this HttpResponse response, TValue value, @@ -85,16 +72,9 @@ public static Task WriteAsJsonAsync( ArgumentNullException.ThrowIfNull(response); options ??= ResolveSerializerOptions(response.HttpContext); + options.EnsureConfigured(); - response.ContentType = contentType ?? JsonConstants.JsonContentTypeWithCharset; - - // if no user provided token, pass the RequestAborted token and ignore OperationCanceledException - if (!cancellationToken.CanBeCanceled) - { - return WriteAsJsonAsyncSlow(response.Body, value, options, response.HttpContext.RequestAborted); - } - - return JsonSerializer.SerializeAsync(response.Body, value, options, cancellationToken); + return WriteAsJsonAsync(response, value, (JsonTypeInfo)options.GetTypeInfo(typeof(TValue)), contentType, cancellationToken); } /// @@ -141,58 +121,20 @@ static async Task WriteAsJsonAsyncSlow(HttpResponse response, TValue value, Json /// /// Write the specified value as JSON to the response body. The response content-type will be set to - /// the specified content-type. + /// application/json; charset=utf-8. /// /// The response to write JSON to. /// The value to write as JSON. - /// Metadata about the type to convert. - /// The content-type to set on the response. + /// The type of object to write. /// A used to cancel the operation. /// The task object representing the asynchronous operation. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static Task WriteAsJsonAsync( -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters this HttpResponse response, object? value, - JsonTypeInfo jsonTypeInfo, - string? contentType = default, + Type type, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(response); - - response.ContentType = contentType ?? JsonConstants.JsonContentTypeWithCharset; - - // if no user provided token, pass the RequestAborted token and ignore OperationCanceledException - if (!cancellationToken.CanBeCanceled) - { - return WriteAsJsonAsyncSlow(response, value, jsonTypeInfo); - } - - return JsonSerializer.SerializeAsync(response.Body, value, jsonTypeInfo, cancellationToken); - - static async Task WriteAsJsonAsyncSlow(HttpResponse response, object? value, JsonTypeInfo jsonTypeInfo) - { - try - { - await JsonSerializer.SerializeAsync(response.Body, value, jsonTypeInfo, response.HttpContext.RequestAborted); - } - catch (OperationCanceledException) { } - } - } - - [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] - [RequiresDynamicCode(RequiresDynamicCodeMessage)] - private static async Task WriteAsJsonAsyncSlow( - Stream body, - TValue value, - JsonSerializerOptions? options, - CancellationToken cancellationToken) - { - try - { - await JsonSerializer.SerializeAsync(body, value, options, cancellationToken); - } - catch (OperationCanceledException) { } + return response.WriteAsJsonAsync(value, type, options: null, contentType: null, cancellationToken); } /// @@ -202,40 +144,45 @@ private static async Task WriteAsJsonAsyncSlow( /// The response to write JSON to. /// The value to write as JSON. /// The type of object to write. + /// The serializer options to use when serializing the value. /// A used to cancel the operation. /// The task object representing the asynchronous operation. public static Task WriteAsJsonAsync( this HttpResponse response, object? value, Type type, + JsonSerializerOptions? options, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(response); - - var options = ResolveSerializerOptions(response.HttpContext); - return response.WriteAsJsonAsync(value, jsonTypeInfo: options.GetTypeInfo(type), contentType: null, cancellationToken); + return response.WriteAsJsonAsync(value, type, options, contentType: null, cancellationToken); } /// /// Write the specified value as JSON to the response body. The response content-type will be set to - /// application/json; charset=utf-8. + /// the specified content-type. /// /// The response to write JSON to. /// The value to write as JSON. /// The type of object to write. /// The serializer options to use when serializing the value. + /// The content-type to set on the response. /// A used to cancel the operation. /// The task object representing the asynchronous operation. - [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] - [RequiresDynamicCode(RequiresDynamicCodeMessage)] public static Task WriteAsJsonAsync( this HttpResponse response, object? value, Type type, JsonSerializerOptions? options, + string? contentType, CancellationToken cancellationToken = default) { - return response.WriteAsJsonAsync(value, type, options, contentType: null, cancellationToken); + ArgumentNullException.ThrowIfNull(response); + ArgumentNullException.ThrowIfNull(type); + + options ??= ResolveSerializerOptions(response.HttpContext); + options.EnsureConfigured(); + + return WriteAsJsonAsync(response, value, options.GetTypeInfo(type), contentType, cancellationToken); } /// @@ -245,50 +192,42 @@ public static Task WriteAsJsonAsync( /// The response to write JSON to. /// The value to write as JSON. /// The type of object to write. - /// The serializer options to use when serializing the value. + /// A metadata provider for serializable types. /// The content-type to set on the response. /// A used to cancel the operation. /// The task object representing the asynchronous operation. - [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] - [RequiresDynamicCode(RequiresDynamicCodeMessage)] +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static Task WriteAsJsonAsync( +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters this HttpResponse response, object? value, Type type, - JsonSerializerOptions? options, - string? contentType, + JsonSerializerContext context, + string? contentType = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(response); ArgumentNullException.ThrowIfNull(type); - - options ??= ResolveSerializerOptions(response.HttpContext); + ArgumentNullException.ThrowIfNull(context); response.ContentType = contentType ?? JsonConstants.JsonContentTypeWithCharset; // if no user provided token, pass the RequestAborted token and ignore OperationCanceledException if (!cancellationToken.CanBeCanceled) { - return WriteAsJsonAsyncSlow(response.Body, value, type, options, response.HttpContext.RequestAborted); + return WriteAsJsonAsyncSlow(); } - return JsonSerializer.SerializeAsync(response.Body, value, type, options, cancellationToken); - } + return JsonSerializer.SerializeAsync(response.Body, value, type, context, cancellationToken); - [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] - [RequiresDynamicCode(RequiresDynamicCodeMessage)] - private static async Task WriteAsJsonAsyncSlow( - Stream body, - object? value, - Type type, - JsonSerializerOptions? options, - CancellationToken cancellationToken) - { - try + async Task WriteAsJsonAsyncSlow() { - await JsonSerializer.SerializeAsync(body, value, type, options, cancellationToken); + try + { + await JsonSerializer.SerializeAsync(response.Body, value, type, context, cancellationToken); + } + catch (OperationCanceledException) { } } - catch (OperationCanceledException) { } } /// @@ -297,8 +236,7 @@ private static async Task WriteAsJsonAsyncSlow( /// /// The response to write JSON to. /// The value to write as JSON. - /// The type of object to write. - /// A metadata provider for serializable types. + /// Metadata about the type to convert. /// The content-type to set on the response. /// A used to cancel the operation. /// The task object representing the asynchronous operation. @@ -307,30 +245,27 @@ public static Task WriteAsJsonAsync( #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters this HttpResponse response, object? value, - Type type, - JsonSerializerContext context, + JsonTypeInfo jsonTypeInfo, string? contentType = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(response); - ArgumentNullException.ThrowIfNull(type); - ArgumentNullException.ThrowIfNull(context); response.ContentType = contentType ?? JsonConstants.JsonContentTypeWithCharset; // if no user provided token, pass the RequestAborted token and ignore OperationCanceledException if (!cancellationToken.CanBeCanceled) { - return WriteAsJsonAsyncSlow(); + return WriteAsJsonAsyncSlow(response, value, jsonTypeInfo); } - return JsonSerializer.SerializeAsync(response.Body, value, type, context, cancellationToken); + return JsonSerializer.SerializeAsync(response.Body, value, jsonTypeInfo, cancellationToken); - async Task WriteAsJsonAsyncSlow() + static async Task WriteAsJsonAsyncSlow(HttpResponse response, object? value, JsonTypeInfo jsonTypeInfo) { try { - await JsonSerializer.SerializeAsync(response.Body, value, type, context, cancellationToken); + await JsonSerializer.SerializeAsync(response.Body, value, jsonTypeInfo, response.HttpContext.RequestAborted); } catch (OperationCanceledException) { } } diff --git a/src/Http/Http.Extensions/src/JsonOptions.cs b/src/Http/Http.Extensions/src/JsonOptions.cs index dfd572b464fe..7f65b033429e 100644 --- a/src/Http/Http.Extensions/src/JsonOptions.cs +++ b/src/Http/Http.Extensions/src/JsonOptions.cs @@ -3,7 +3,6 @@ using System.Text.Encodings.Web; using System.Text.Json; -using Microsoft.AspNetCore.Internal; #nullable enable @@ -30,32 +29,11 @@ public class JsonOptions /// /// Gets the . /// - public JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions(DefaultSerializerOptions); + public JsonSerializerOptions SerializerOptions { get; private init; } = new JsonSerializerOptions(DefaultSerializerOptions); /// /// Gets the default instance. /// - public static JsonOptions Default { get => _defaultInstance ??= new JsonOptions().EnsureConfigured(); } + public static JsonOptions Default { get => _defaultInstance ??= new JsonOptions() { SerializerOptions = new JsonSerializerOptions(DefaultSerializerOptions).EnsureConfigured(markAsReadOnly: true) }; } - internal JsonOptions EnsureConfigured(bool markAsReadOnly = true) - { - if (!SerializerOptions.IsReadOnly) - { - if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) - { -#pragma warning disable IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml -#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. - SerializerOptions.InitializeForReflection(); -#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. -#pragma warning restore IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml - } - - if (markAsReadOnly) - { - SerializerOptions.MakeReadOnly(); - } - } - - return this; - } } diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml index 924b3ec9949e..be2287df7423 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml @@ -5,8 +5,8 @@ ILLink IL2026 member - M:Microsoft.AspNetCore.Http.Json.JsonOptions.EnsureConfigured(System.Boolean) + M:Microsoft.AspNetCore.Http.JsonSerializerOptionsExtensions.EnsureConfigured(System.Text.Json.JsonSerializerOptions,System.Boolean) This warning is left in the product so developers get an ILLink warning when trimming an app only when Microsoft.AspNetCore.EnsureJsonTrimmability=false. - \ No newline at end of file + diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj index 483b75768e13..0455adcf1058 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 41c7523a02d5..477a10c0cc54 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -263,7 +263,7 @@ private static RequestDelegateFactoryContext CreateFactoryContext(RequestDelegat var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices ?? EmptyServiceProvider.Instance; var endpointBuilder = options?.EndpointBuilder ?? new RdfEndpointBuilder(serviceProvider); var jsonSerializerOptions = serviceProvider.GetService>()?.Value.SerializerOptions ?? JsonOptions.Default.SerializerOptions; - jsonSerializerOptions.MakeReadOnly(); + jsonSerializerOptions.EnsureConfigured(markAsReadOnly: true); var factoryContext = new RequestDelegateFactoryContext { @@ -1001,24 +1001,21 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, ExecuteAwaitedReturnMethod, methodCall, HttpContextExpr, - factoryContext.JsonSerializerOptionsExpression, - Expression.Constant(factoryContext.JsonSerializerOptions.GetTypeInfo(typeof(object)), typeof(JsonTypeInfo))); + factoryContext.JsonSerializerOptionsExpression); } else if (returnType == typeof(ValueTask)) { return Expression.Call(ExecuteValueTaskOfObjectMethod, methodCall, HttpContextExpr, - factoryContext.JsonSerializerOptionsExpression, - Expression.Constant(factoryContext.JsonSerializerOptions.GetTypeInfo(typeof(object)), typeof(JsonTypeInfo))); + factoryContext.JsonSerializerOptionsExpression); } else if (returnType == typeof(Task)) { return Expression.Call(ExecuteTaskOfObjectMethod, methodCall, HttpContextExpr, - factoryContext.JsonSerializerOptionsExpression, - Expression.Constant(factoryContext.JsonSerializerOptions.GetTypeInfo(typeof(object)), typeof(JsonTypeInfo))); + factoryContext.JsonSerializerOptionsExpression); } else if (AwaitableInfo.IsTypeAwaitable(returnType, out _)) { @@ -2094,39 +2091,39 @@ private static MemberInfo GetMemberInfo(Expression expr) // if necessary and restart the cycle until we've reached a terminal state (unknown type). // We currently don't handle Task or ValueTask. We can support this later if this // ends up being a common scenario. - private static Task ExecuteValueTaskOfObject(ValueTask valueTask, HttpContext httpContext, JsonSerializerOptions options, JsonTypeInfo jsonTypeInfo) + private static Task ExecuteValueTaskOfObject(ValueTask valueTask, HttpContext httpContext, JsonSerializerOptions options) { - static async Task ExecuteAwaited(ValueTask valueTask, HttpContext httpContext, JsonSerializerOptions options, JsonTypeInfo jsonTypeInfo) + static async Task ExecuteAwaited(ValueTask valueTask, HttpContext httpContext, JsonSerializerOptions options) { - await ExecuteAwaitedReturn(await valueTask, httpContext, options, jsonTypeInfo); + await ExecuteAwaitedReturn(await valueTask, httpContext, options); } if (valueTask.IsCompletedSuccessfully) { - return ExecuteAwaitedReturn(valueTask.GetAwaiter().GetResult(), httpContext, options, jsonTypeInfo); + return ExecuteAwaitedReturn(valueTask.GetAwaiter().GetResult(), httpContext, options); } - return ExecuteAwaited(valueTask, httpContext, options, jsonTypeInfo); + return ExecuteAwaited(valueTask, httpContext, options); } - private static Task ExecuteTaskOfObject(Task task, HttpContext httpContext, JsonSerializerOptions options, JsonTypeInfo jsonTypeInfo) + private static Task ExecuteTaskOfObject(Task task, HttpContext httpContext, JsonSerializerOptions options) { - static async Task ExecuteAwaited(Task task, HttpContext httpContext, JsonSerializerOptions options, JsonTypeInfo jsonTypeInfo) + static async Task ExecuteAwaited(Task task, HttpContext httpContext, JsonSerializerOptions options) { - await ExecuteAwaitedReturn(await task, httpContext, options, jsonTypeInfo); + await ExecuteAwaitedReturn(await task, httpContext, options); } if (task.IsCompletedSuccessfully) { - return ExecuteAwaitedReturn(task.GetAwaiter().GetResult(), httpContext, options, jsonTypeInfo); + return ExecuteAwaitedReturn(task.GetAwaiter().GetResult(), httpContext, options); } - return ExecuteAwaited(task, httpContext, options, jsonTypeInfo); + return ExecuteAwaited(task, httpContext, options); } - private static Task ExecuteAwaitedReturn(object obj, HttpContext httpContext, JsonSerializerOptions options, JsonTypeInfo jsonTypeInfo) + private static Task ExecuteAwaitedReturn(object obj, HttpContext httpContext, JsonSerializerOptions options) { - return ExecuteHandlerHelper.ExecuteReturnAsync(obj, httpContext, options, jsonTypeInfo); + return ExecuteHandlerHelper.ExecuteReturnAsync(obj, httpContext, options); } private static Task ExecuteTaskOfTFast(Task task, HttpContext httpContext, JsonTypeInfo jsonTypeInfo) diff --git a/src/Http/Http.Extensions/test/JsonOptionsTests.cs b/src/Http/Http.Extensions/test/JsonOptionsTests.cs index e3c446c501c2..93a5c8103a76 100644 --- a/src/Http/Http.Extensions/test/JsonOptionsTests.cs +++ b/src/Http/Http.Extensions/test/JsonOptionsTests.cs @@ -12,92 +12,92 @@ namespace Microsoft.AspNetCore.Http.Extensions.Tests; public partial class JsonOptionsTests { - [ConditionalFact] - [RemoteExecutionSupported] - public void Configure_ThrowsForNullTypeInfoResolver_WhenEnsureJsonTrimmabilityTrue() - { - var options = new RemoteInvokeOptions(); - options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); - - using var remoteHandle = RemoteExecutor.Invoke(static () => - { - // Arrange - var options = new JsonSerializerOptions(); - - // Act & Assert - Assert.Throws(() => new JsonOptions().EnsureConfigured()); - }, options); - } - - [ConditionalFact] - [RemoteExecutionSupported] - public void Configure_Works_WhenEnsureJsonTrimmabilityTrue() - { - var options = new RemoteInvokeOptions(); - options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); - - using var remoteHandle = RemoteExecutor.Invoke(static () => - { - // Arrange - var options = new JsonOptions(); - options.SerializerOptions.AddContext(); - - // Act - var jsonOptions = options.EnsureConfigured(); - - // Assert - var serializerOptions = options.SerializerOptions; - Assert.NotNull(serializerOptions.TypeInfoResolver); - Assert.IsType(serializerOptions.TypeInfoResolver); - Assert.True(serializerOptions.IsReadOnly); - }, options); - } - - [ConditionalFact] - [RemoteExecutionSupported] - public void DefaultSerializerOptions_Works_WhenEnsureJsonTrimmabilityFalse() - { - var options = new RemoteInvokeOptions(); - options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); - - using var remoteHandle = RemoteExecutor.Invoke(static () => - { - // Act - var options = new JsonOptions().EnsureConfigured(); - - // Assert - var serializerOptions = options.SerializerOptions; - Assert.NotNull(serializerOptions.TypeInfoResolver); - Assert.IsType(serializerOptions.TypeInfoResolver); - Assert.True(serializerOptions.IsReadOnly); - }, options); - } - - [ConditionalFact] - [RemoteExecutionSupported] - public void DefaultSerializerOptions_Combines_WhenEnsureJsonTrimmabilityFalse() - { - var options = new RemoteInvokeOptions(); - options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); - - using var remoteHandle = RemoteExecutor.Invoke(static () => - { - // Arrange - var options = new JsonOptions(); - options.SerializerOptions.AddContext(); - - // Act - var jsonOptions = options.EnsureConfigured(); - - // Assert - var serializerOptions = options.SerializerOptions; - Assert.NotNull(serializerOptions.TypeInfoResolver); - Assert.IsNotType(serializerOptions.TypeInfoResolver); - Assert.IsNotType(serializerOptions.TypeInfoResolver); - Assert.NotNull(serializerOptions.TypeInfoResolver.GetTypeInfo(typeof(string), serializerOptions)); - Assert.True(serializerOptions.IsReadOnly); - }, options); - } + //[ConditionalFact] + //[RemoteExecutionSupported] + //public void Configure_ThrowsForNullTypeInfoResolver_WhenEnsureJsonTrimmabilityTrue() + //{ + // var options = new RemoteInvokeOptions(); + // options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); + + // using var remoteHandle = RemoteExecutor.Invoke(static () => + // { + // // Arrange + // var options = new JsonSerializerOptions(); + + // // Act & Assert + // Assert.Throws(() => new JsonOptions().EnsureConfigured()); + // }, options); + //} + + //[ConditionalFact] + //[RemoteExecutionSupported] + //public void Configure_Works_WhenEnsureJsonTrimmabilityTrue() + //{ + // var options = new RemoteInvokeOptions(); + // options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); + + // using var remoteHandle = RemoteExecutor.Invoke(static () => + // { + // // Arrange + // var options = new JsonOptions(); + // options.SerializerOptions.AddContext(); + + // // Act + // var jsonOptions = options.EnsureConfigured(); + + // // Assert + // var serializerOptions = options.SerializerOptions; + // Assert.NotNull(serializerOptions.TypeInfoResolver); + // Assert.IsType(serializerOptions.TypeInfoResolver); + // Assert.True(serializerOptions.IsReadOnly); + // }, options); + //} + + //[ConditionalFact] + //[RemoteExecutionSupported] + //public void DefaultSerializerOptions_Works_WhenEnsureJsonTrimmabilityFalse() + //{ + // var options = new RemoteInvokeOptions(); + // options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); + + // using var remoteHandle = RemoteExecutor.Invoke(static () => + // { + // // Act + // var options = new JsonOptions().EnsureConfigured(); + + // // Assert + // var serializerOptions = options.SerializerOptions; + // Assert.NotNull(serializerOptions.TypeInfoResolver); + // Assert.IsType(serializerOptions.TypeInfoResolver); + // Assert.True(serializerOptions.IsReadOnly); + // }, options); + //} + + //[ConditionalFact] + //[RemoteExecutionSupported] + //public void DefaultSerializerOptions_Combines_WhenEnsureJsonTrimmabilityFalse() + //{ + // var options = new RemoteInvokeOptions(); + // options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); + + // using var remoteHandle = RemoteExecutor.Invoke(static () => + // { + // // Arrange + // var options = new JsonOptions(); + // options.SerializerOptions.AddContext(); + + // // Act + // var jsonOptions = options.EnsureConfigured(); + + // // Assert + // var serializerOptions = options.SerializerOptions; + // Assert.NotNull(serializerOptions.TypeInfoResolver); + // Assert.IsNotType(serializerOptions.TypeInfoResolver); + // Assert.IsNotType(serializerOptions.TypeInfoResolver); + // Assert.NotNull(serializerOptions.TypeInfoResolver.GetTypeInfo(typeof(string), serializerOptions)); + // Assert.True(serializerOptions.IsReadOnly); + // }, options); + //} [JsonSerializable(typeof(object))] private partial class JsonSerializerExtensionsTestsContext : JsonSerializerContext diff --git a/src/Http/Http.Results/src/JsonHttpResultOfT.cs b/src/Http/Http.Results/src/JsonHttpResultOfT.cs index a9909d19f3a1..4841e675ecfd 100644 --- a/src/Http/Http.Results/src/JsonHttpResultOfT.cs +++ b/src/Http/Http.Results/src/JsonHttpResultOfT.cs @@ -38,7 +38,7 @@ internal JsonHttpResult(TValue? value, JsonSerializerOptions? jsonSerializerOpti if (jsonSerializerOptions is not null && !jsonSerializerOptions.IsReadOnly) { - jsonSerializerOptions.InitializeForReflection(); + jsonSerializerOptions.TypeInfoResolver ??= new DefaultJsonTypeInfoResolver(); } JsonSerializerOptions = jsonSerializerOptions; diff --git a/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs index fc6cbeb76f86..6c912748bca5 100644 --- a/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs +++ b/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs @@ -98,9 +98,6 @@ public static IServiceCollection AddRouting(this IServiceCollection services) // Set RouteHandlerOptions.ThrowOnBadRequest in development services.TryAddEnumerable(ServiceDescriptor.Transient, ConfigureRouteHandlerOptions>()); - // Set defaults for Http.JsonOptions required for RDF - services.AddDefaultHttpJsonOptions(); - return services; } diff --git a/src/Http/Routing/src/RequestDelegateFilterPipelineBuilder.cs b/src/Http/Routing/src/RequestDelegateFilterPipelineBuilder.cs index af6d359a729c..df3f15aa48e8 100644 --- a/src/Http/Routing/src/RequestDelegateFilterPipelineBuilder.cs +++ b/src/Http/Routing/src/RequestDelegateFilterPipelineBuilder.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.Json; @@ -20,14 +19,12 @@ public static RequestDelegate Create(RequestDelegate requestDelegate, RequestDel var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; var jsonSerializerOptions = serviceProvider?.GetService>()?.Value.SerializerOptions ?? JsonOptions.Default.SerializerOptions; - jsonSerializerOptions.MakeReadOnly(); var factoryContext = new EndpointFilterFactoryContext { MethodInfo = requestDelegate.Method, ApplicationServices = options.EndpointBuilder.ApplicationServices }; - var jsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(object)); EndpointFilterDelegate filteredInvocation = async (EndpointFilterInvocationContext context) => { @@ -56,7 +53,7 @@ public static RequestDelegate Create(RequestDelegate requestDelegate, RequestDel var obj = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext, new object[] { httpContext })); if (obj is not null) { - await ExecuteHandlerHelper.ExecuteReturnAsync(obj, httpContext, jsonSerializerOptions, jsonTypeInfo); + await ExecuteHandlerHelper.ExecuteReturnAsync(obj, httpContext, jsonSerializerOptions); } }; } diff --git a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs index 1cf6db74f5cf..be45c993b0b3 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs @@ -21,6 +21,7 @@ public class SystemTextJsonOutputFormatter : TextOutputFormatter /// The . public SystemTextJsonOutputFormatter(JsonSerializerOptions jsonSerializerOptions) { + jsonSerializerOptions.EnsureConfigured(markAsReadOnly: true); SerializerOptions = jsonSerializerOptions; SupportedEncodings.Add(Encoding.UTF8); @@ -32,8 +33,6 @@ public SystemTextJsonOutputFormatter(JsonSerializerOptions jsonSerializerOptions internal static SystemTextJsonOutputFormatter CreateFormatter(JsonOptions jsonOptions) { - jsonOptions.EnsureConfigured(); - var jsonSerializerOptions = jsonOptions.JsonSerializerOptions; if (jsonSerializerOptions.Encoder is null) diff --git a/src/Mvc/Mvc.Core/src/JsonOptions.cs b/src/Mvc/Mvc.Core/src/JsonOptions.cs index 397a2f980ee8..d3966142f027 100644 --- a/src/Mvc/Mvc.Core/src/JsonOptions.cs +++ b/src/Mvc/Mvc.Core/src/JsonOptions.cs @@ -2,10 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text.Json; -using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Mvc; @@ -40,19 +38,4 @@ public class JsonOptions // This value is the same for model binding and Json.Net's serialization. MaxDepth = MvcOptions.DefaultMaxModelBindingRecursionDepth, }; - - internal JsonOptions EnsureConfigured() - { - if (!JsonSerializerOptions.IsReadOnly) - { - if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) - { - JsonSerializerOptions.InitializeForReflection(); - } - - JsonSerializerOptions.MakeReadOnly(); - } - - return this; - } } diff --git a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj index 161ce2ddcebf..18b4fd58a224 100644 --- a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj @@ -34,7 +34,7 @@ Microsoft.AspNetCore.Mvc.RouteAttribute - + diff --git a/src/Shared/Json/JsonSerializerExtensions.cs b/src/Shared/Json/JsonSerializerExtensions.cs index 3fa26a0b9a23..53e0229d3ada 100644 --- a/src/Shared/Json/JsonSerializerExtensions.cs +++ b/src/Shared/Json/JsonSerializerExtensions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -10,8 +9,6 @@ namespace Microsoft.AspNetCore.Http; internal static class JsonSerializerExtensions { - private static DefaultJsonTypeInfoResolver? _defaultJsonTypeInfoResolver; - public static bool HasKnownPolymorphism(this JsonTypeInfo jsonTypeInfo) => jsonTypeInfo.Type.IsSealed || jsonTypeInfo.Type.IsValueType || jsonTypeInfo.PolymorphismOptions is not null; @@ -20,17 +17,4 @@ public static bool IsValid(this JsonTypeInfo jsonTypeInfo, [NotNullWhen(false)] public static JsonTypeInfo GetRequiredTypeInfo(this JsonSerializerContext context, Type type) => context.GetTypeInfo(type) ?? throw new InvalidOperationException($"Unable to obtain the JsonTypeInfo for type '{type.FullName}' from the context '{context.GetType().FullName}'."); - - [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] - [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true.")] - public static void InitializeForReflection(this JsonSerializerOptions options) - { - _defaultJsonTypeInfoResolver ??= new DefaultJsonTypeInfoResolver(); - - options.TypeInfoResolver = options.TypeInfoResolver switch - { - null => _defaultJsonTypeInfoResolver, - _ => JsonTypeInfoResolver.Combine(options.TypeInfoResolver, _defaultJsonTypeInfoResolver), - }; - } } diff --git a/src/Shared/Json/JsonSerializerOptionsExtensions.cs b/src/Shared/Json/JsonSerializerOptionsExtensions.cs new file mode 100644 index 000000000000..a95d8e78104e --- /dev/null +++ b/src/Shared/Json/JsonSerializerOptionsExtensions.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization.Metadata; +using System.Text.Json; +using Microsoft.AspNetCore.Internal; + +namespace Microsoft.AspNetCore.Http; + +internal static class JsonSerializerOptionsExtensions +{ + private static DefaultJsonTypeInfoResolver? _defaultJsonTypeInfoResolver; + + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use the overload that takes a JsonTypeInfo, or make sure all of the required types are preserved.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true for native AOT applications.")] + public static void InitializeForReflection(this JsonSerializerOptions options) + { + _defaultJsonTypeInfoResolver ??= new DefaultJsonTypeInfoResolver(); + + options.TypeInfoResolver = options.TypeInfoResolver switch + { + null => _defaultJsonTypeInfoResolver, + _ => JsonTypeInfoResolver.Combine(options.TypeInfoResolver, _defaultJsonTypeInfoResolver), + }; + } + + public static JsonSerializerOptions EnsureConfigured(this JsonSerializerOptions options, bool markAsReadOnly = false) + { + if (!options.IsReadOnly) + { + if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) + { +#pragma warning disable IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml +#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. + options.InitializeForReflection(); +#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. +#pragma warning restore IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml + } + + if (markAsReadOnly) + { + options.MakeReadOnly(); + } + } + + return options; + } +} diff --git a/src/Shared/RouteHandlers/ExecuteHandlerHelper.cs b/src/Shared/RouteHandlers/ExecuteHandlerHelper.cs index 12217930d751..f2f0d657bfba 100644 --- a/src/Shared/RouteHandlers/ExecuteHandlerHelper.cs +++ b/src/Shared/RouteHandlers/ExecuteHandlerHelper.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Internal; internal static class ExecuteHandlerHelper { - public static Task ExecuteReturnAsync(object obj, HttpContext httpContext, JsonSerializerOptions options, JsonTypeInfo jsonTypeInfo) + public static Task ExecuteReturnAsync(object obj, HttpContext httpContext, JsonSerializerOptions options) { // Terminal built ins if (obj is IResult result) @@ -24,7 +24,8 @@ public static Task ExecuteReturnAsync(object obj, HttpContext httpContext, JsonS else { // Otherwise, we JSON serialize when we reach the terminal state - return WriteJsonResponseAsync(httpContext.Response, obj, options, jsonTypeInfo); + var runtimeType = obj.GetType() ?? typeof(object); + return HttpResponseJsonExtensions.WriteAsJsonAsync(httpContext.Response, obj, runtimeType, options, default); } } From fe7434ae74f419b3710c4a53b9bb0fb1eb118801 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 9 Feb 2023 23:14:16 -0800 Subject: [PATCH 33/48] Clean up --- src/Security/Authentication/test/JwtBearerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Security/Authentication/test/JwtBearerTests.cs b/src/Security/Authentication/test/JwtBearerTests.cs index 1fa1cf2a93cd..6f2160830053 100755 --- a/src/Security/Authentication/test/JwtBearerTests.cs +++ b/src/Security/Authentication/test/JwtBearerTests.cs @@ -1174,7 +1174,7 @@ private static async Task CreateHost(Action options = n { var authenticationResult = await context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); await context.Response.WriteAsJsonAsync( - new { Expires = authenticationResult.Properties?.ExpiresUtc, Issued = authenticationResult.Properties?.IssuedUtc }, Http.Json.JsonOptions.Default.SerializerOptions); + new { Expires = authenticationResult.Properties?.ExpiresUtc, Issued = authenticationResult.Properties?.IssuedUtc }); } else { From 580d227436486b1056ab2e8442e25d8ecfbed06a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 10 Feb 2023 00:08:36 -0800 Subject: [PATCH 34/48] More clean up --- .../Http.Extensions/src/HttpRequestJsonExtensions.cs | 2 +- .../Http.Extensions/src/HttpResponseJsonExtensions.cs | 2 +- src/Http/Http.Extensions/src/JsonOptions.cs | 9 ++++----- src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt | 2 +- src/Http/Http.Extensions/src/RequestDelegateFactory.cs | 2 +- .../Http.Extensions/test/RequestDelegateFactoryTests.cs | 6 +++--- src/Http/Http.Results/src/HttpResultsHelper.cs | 9 ++------- .../Routing/src/RequestDelegateFilterPipelineBuilder.cs | 2 +- .../DeveloperExceptionPageMiddlewareImpl.cs | 3 +-- 9 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs index 54f758da87ee..044ea546fd9f 100644 --- a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs @@ -261,7 +261,7 @@ private static bool HasJsonContentType(this HttpRequest request, out StringSegme private static JsonSerializerOptions ResolveSerializerOptions(HttpContext httpContext) { // Attempt to resolve options from DI then fallback to default options - return httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions ?? JsonOptions.Default.SerializerOptions; + return httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; } [DoesNotReturn] diff --git a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs index 9d4cd9c0942c..2442cee133af 100644 --- a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs @@ -274,6 +274,6 @@ static async Task WriteAsJsonAsyncSlow(HttpResponse response, object? value, Jso private static JsonSerializerOptions ResolveSerializerOptions(HttpContext httpContext) { // Attempt to resolve options from DI then fallback to default options - return httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions ?? JsonOptions.Default.SerializerOptions; + return httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; } } diff --git a/src/Http/Http.Extensions/src/JsonOptions.cs b/src/Http/Http.Extensions/src/JsonOptions.cs index 7f65b033429e..cf4af1a714bc 100644 --- a/src/Http/Http.Extensions/src/JsonOptions.cs +++ b/src/Http/Http.Extensions/src/JsonOptions.cs @@ -14,9 +14,8 @@ namespace Microsoft.AspNetCore.Http.Json; /// public class JsonOptions { - private static JsonOptions? _defaultInstance; - - private static readonly JsonSerializerOptions DefaultSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) + private static JsonSerializerOptions? _defaultSerializerOptionsInstance; + private static readonly JsonSerializerOptions _defaultSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) { // Web defaults don't use the relex JSON escaping encoder. // @@ -29,11 +28,11 @@ public class JsonOptions /// /// Gets the . /// - public JsonSerializerOptions SerializerOptions { get; private init; } = new JsonSerializerOptions(DefaultSerializerOptions); + public JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions(_defaultSerializerOptions); /// /// Gets the default instance. /// - public static JsonOptions Default { get => _defaultInstance ??= new JsonOptions() { SerializerOptions = new JsonSerializerOptions(DefaultSerializerOptions).EnsureConfigured(markAsReadOnly: true) }; } + public static JsonSerializerOptions DefaultSerializerOptions { get => _defaultSerializerOptionsInstance ??= new JsonSerializerOptions(_defaultSerializerOptions).EnsureConfigured(markAsReadOnly: true); } } diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index 5b837db4c608..5bae97f14959 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,5 +1,5 @@ #nullable enable static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, object? value, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, string? contentType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.AspNetCore.Http.Json.JsonOptions.Default.get -> Microsoft.AspNetCore.Http.Json.JsonOptions! +static Microsoft.AspNetCore.Http.Json.JsonOptions.DefaultSerializerOptions.get -> System.Text.Json.JsonSerializerOptions! static Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.AddDefaultHttpJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 477a10c0cc54..747bf5e88e2a 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -262,7 +262,7 @@ private static RequestDelegateFactoryContext CreateFactoryContext(RequestDelegat var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices ?? EmptyServiceProvider.Instance; var endpointBuilder = options?.EndpointBuilder ?? new RdfEndpointBuilder(serviceProvider); - var jsonSerializerOptions = serviceProvider.GetService>()?.Value.SerializerOptions ?? JsonOptions.Default.SerializerOptions; + var jsonSerializerOptions = serviceProvider.GetService>()?.Value.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; jsonSerializerOptions.EnsureConfigured(markAsReadOnly: true); var factoryContext = new RequestDelegateFactoryContext diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index faee0bf6faac..5dcf83e38f2a 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -3132,7 +3132,7 @@ public async Task RequestDelegateWritesMembersFromChildTypesToJsonResponseBody_W var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) - .AddSingleton(Options.Create(JsonOptions.Default)) + .AddSingleton(Options.Create(new JsonOptions())) .BuildServiceProvider(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -3159,7 +3159,7 @@ public async Task RequestDelegateWritesMembersFromChildTypesToJsonResponseBody_W var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) - .AddSingleton(Options.Create(JsonOptions.Default)) + .AddSingleton(Options.Create(new JsonOptions())) .BuildServiceProvider(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -3186,7 +3186,7 @@ public async Task RequestDelegateWritesJsonTypeDiscriminatorToJsonResponseBody_W var httpContext = CreateHttpContext(); httpContext.RequestServices = new ServiceCollection() .AddSingleton(LoggerFactory) - .AddSingleton(Options.Create(JsonOptions.Default)) + .AddSingleton(Options.Create(new JsonOptions())) .BuildServiceProvider(); var responseBodyStream = new MemoryStream(); diff --git a/src/Http/Http.Results/src/HttpResultsHelper.cs b/src/Http/Http.Results/src/HttpResultsHelper.cs index b0e937383802..c91e8767dcd4 100644 --- a/src/Http/Http.Results/src/HttpResultsHelper.cs +++ b/src/Http/Http.Results/src/HttpResultsHelper.cs @@ -31,7 +31,8 @@ public static Task WriteResultAsJsonAsync( return Task.CompletedTask; } - jsonSerializerOptions ??= ResolveJsonOptions(httpContext).SerializerOptions; + jsonSerializerOptions ??= httpContext.RequestServices.GetService>()?.Value.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; + //TODO: EnsureConfigured var jsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(TValue)); @@ -145,12 +146,6 @@ public static void ApplyProblemDetailsDefaultsIfNeeded(object? value, int? statu } } - private static JsonOptions ResolveJsonOptions(HttpContext httpContext) - { - // Attempt to resolve options from DI then fallback to default options - return httpContext.RequestServices.GetService>()?.Value ?? JsonOptions.Default; - } - internal static partial class Log { [LoggerMessage(1, LogLevel.Information, diff --git a/src/Http/Routing/src/RequestDelegateFilterPipelineBuilder.cs b/src/Http/Routing/src/RequestDelegateFilterPipelineBuilder.cs index df3f15aa48e8..d298bd2f315d 100644 --- a/src/Http/Routing/src/RequestDelegateFilterPipelineBuilder.cs +++ b/src/Http/Routing/src/RequestDelegateFilterPipelineBuilder.cs @@ -18,7 +18,7 @@ public static RequestDelegate Create(RequestDelegate requestDelegate, RequestDel Debug.Assert(options.EndpointBuilder != null); var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; - var jsonSerializerOptions = serviceProvider?.GetService>()?.Value.SerializerOptions ?? JsonOptions.Default.SerializerOptions; + var jsonSerializerOptions = serviceProvider?.GetService>()?.Value.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; var factoryContext = new EndpointFilterFactoryContext { diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs index 7c669a7dfca0..3431d06954ba 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs @@ -84,8 +84,7 @@ public DeveloperExceptionPageMiddlewareImpl( private static ExtensionsExceptionJsonContext CreateSerializationContext(JsonOptions? jsonOptions) { // Create context from configured options to get settings such as PropertyNamePolicy and DictionaryKeyPolicy. - jsonOptions ??= JsonOptions.Default; - return new ExtensionsExceptionJsonContext(new JsonSerializerOptions(jsonOptions.SerializerOptions)); + return new ExtensionsExceptionJsonContext(new JsonSerializerOptions(jsonOptions?.SerializerOptions ?? JsonOptions.DefaultSerializerOptions)); } /// From 6dfe6c1f98a8836d3523e8bc668b95309abf7d9f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 10 Feb 2023 00:10:42 -0800 Subject: [PATCH 35/48] More clean up --- src/Http/Http.Extensions/src/JsonOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/src/JsonOptions.cs b/src/Http/Http.Extensions/src/JsonOptions.cs index cf4af1a714bc..f29af9a5e77e 100644 --- a/src/Http/Http.Extensions/src/JsonOptions.cs +++ b/src/Http/Http.Extensions/src/JsonOptions.cs @@ -31,7 +31,7 @@ public class JsonOptions public JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions(_defaultSerializerOptions); /// - /// Gets the default instance. + /// Gets the default instance. /// public static JsonSerializerOptions DefaultSerializerOptions { get => _defaultSerializerOptionsInstance ??= new JsonSerializerOptions(_defaultSerializerOptions).EnsureConfigured(markAsReadOnly: true); } From afd67622657fb6094c23ef7fa2f696bf1af48639 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 10 Feb 2023 08:44:18 -0800 Subject: [PATCH 36/48] More clean up --- .../Http.Extensions/test/JsonOptionsTests.cs | 179 +++++++++--------- src/Mvc/Mvc.Core/test/JsonOptionsTest.cs | 104 ---------- 2 files changed, 93 insertions(+), 190 deletions(-) delete mode 100644 src/Mvc/Mvc.Core/test/JsonOptionsTest.cs diff --git a/src/Http/Http.Extensions/test/JsonOptionsTests.cs b/src/Http/Http.Extensions/test/JsonOptionsTests.cs index 93a5c8103a76..7cd854ec5c7b 100644 --- a/src/Http/Http.Extensions/test/JsonOptionsTests.cs +++ b/src/Http/Http.Extensions/test/JsonOptionsTests.cs @@ -12,92 +12,99 @@ namespace Microsoft.AspNetCore.Http.Extensions.Tests; public partial class JsonOptionsTests { - //[ConditionalFact] - //[RemoteExecutionSupported] - //public void Configure_ThrowsForNullTypeInfoResolver_WhenEnsureJsonTrimmabilityTrue() - //{ - // var options = new RemoteInvokeOptions(); - // options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); - - // using var remoteHandle = RemoteExecutor.Invoke(static () => - // { - // // Arrange - // var options = new JsonSerializerOptions(); - - // // Act & Assert - // Assert.Throws(() => new JsonOptions().EnsureConfigured()); - // }, options); - //} - - //[ConditionalFact] - //[RemoteExecutionSupported] - //public void Configure_Works_WhenEnsureJsonTrimmabilityTrue() - //{ - // var options = new RemoteInvokeOptions(); - // options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); - - // using var remoteHandle = RemoteExecutor.Invoke(static () => - // { - // // Arrange - // var options = new JsonOptions(); - // options.SerializerOptions.AddContext(); - - // // Act - // var jsonOptions = options.EnsureConfigured(); - - // // Assert - // var serializerOptions = options.SerializerOptions; - // Assert.NotNull(serializerOptions.TypeInfoResolver); - // Assert.IsType(serializerOptions.TypeInfoResolver); - // Assert.True(serializerOptions.IsReadOnly); - // }, options); - //} - - //[ConditionalFact] - //[RemoteExecutionSupported] - //public void DefaultSerializerOptions_Works_WhenEnsureJsonTrimmabilityFalse() - //{ - // var options = new RemoteInvokeOptions(); - // options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); - - // using var remoteHandle = RemoteExecutor.Invoke(static () => - // { - // // Act - // var options = new JsonOptions().EnsureConfigured(); - - // // Assert - // var serializerOptions = options.SerializerOptions; - // Assert.NotNull(serializerOptions.TypeInfoResolver); - // Assert.IsType(serializerOptions.TypeInfoResolver); - // Assert.True(serializerOptions.IsReadOnly); - // }, options); - //} - - //[ConditionalFact] - //[RemoteExecutionSupported] - //public void DefaultSerializerOptions_Combines_WhenEnsureJsonTrimmabilityFalse() - //{ - // var options = new RemoteInvokeOptions(); - // options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); - - // using var remoteHandle = RemoteExecutor.Invoke(static () => - // { - // // Arrange - // var options = new JsonOptions(); - // options.SerializerOptions.AddContext(); - - // // Act - // var jsonOptions = options.EnsureConfigured(); - - // // Assert - // var serializerOptions = options.SerializerOptions; - // Assert.NotNull(serializerOptions.TypeInfoResolver); - // Assert.IsNotType(serializerOptions.TypeInfoResolver); - // Assert.IsNotType(serializerOptions.TypeInfoResolver); - // Assert.NotNull(serializerOptions.TypeInfoResolver.GetTypeInfo(typeof(string), serializerOptions)); - // Assert.True(serializerOptions.IsReadOnly); - // }, options); - //} + [ConditionalFact] + [RemoteExecutionSupported] + public void Configure_ThrowsForNullTypeInfoResolver_WhenEnsureJsonTrimmabilityTrue_AndMarkReadonly() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Act & Assert + Assert.Throws(() => new JsonSerializerOptions().EnsureConfigured(markAsReadOnly: true)); + }, options); + } + + [Fact] + public void Configure_MarkAsReadOnly_WhenRequested() + { + // Arrange + var options = new JsonSerializerOptions(); + + // Act + _ = options.EnsureConfigured(markAsReadOnly: true); + + // Assert + Assert.True(options.IsReadOnly); + } + + [ConditionalFact] + [RemoteExecutionSupported] + public void Configure_Works_WhenEnsureJsonTrimmabilityTrue() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var options = new JsonSerializerOptions(); + options.AddContext(); + + // Act + _ = options.EnsureConfigured(); + + // Assert + Assert.NotNull(options.TypeInfoResolver); + Assert.IsType(options.TypeInfoResolver); + Assert.False(options.IsReadOnly); + }, options); + } + + [ConditionalFact] + [RemoteExecutionSupported] + public void DefaultSerializerOptions_Works_WhenEnsureJsonTrimmabilityFalse() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Act + var options = new JsonSerializerOptions().EnsureConfigured(); + + // Assert + Assert.NotNull(options.TypeInfoResolver); + Assert.IsType(options.TypeInfoResolver); + Assert.False(options.IsReadOnly); + }, options); + } + + [ConditionalFact] + [RemoteExecutionSupported] + public void DefaultSerializerOptions_Combines_WhenEnsureJsonTrimmabilityFalse() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var options = new JsonSerializerOptions(); + options.AddContext(); + + // Act + _ = options.EnsureConfigured(); + + // Assert + Assert.NotNull(options.TypeInfoResolver); + Assert.IsNotType(options.TypeInfoResolver); + Assert.IsNotType(options.TypeInfoResolver); + Assert.NotNull(options.TypeInfoResolver.GetTypeInfo(typeof(string), options)); + Assert.False(options.IsReadOnly); + }, options); + } [JsonSerializable(typeof(object))] private partial class JsonSerializerExtensionsTestsContext : JsonSerializerContext diff --git a/src/Mvc/Mvc.Core/test/JsonOptionsTest.cs b/src/Mvc/Mvc.Core/test/JsonOptionsTest.cs deleted file mode 100644 index d53ee688700d..000000000000 --- a/src/Mvc/Mvc.Core/test/JsonOptionsTest.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; -using Microsoft.AspNetCore.Testing; -using Microsoft.DotNet.RemoteExecutor; - -namespace Microsoft.AspNetCore.Mvc; - -public partial class JsonOptionsTest -{ - [ConditionalFact] - [RemoteExecutionSupported] - public void Configure_ThrowsForNullTypeInfoResolver_WhenEnsureJsonTrimmabilityTrue() - { - var options = new RemoteInvokeOptions(); - options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); - - using var remoteHandle = RemoteExecutor.Invoke(static () => - { - // Arrange - var options = new JsonSerializerOptions(); - - // Act & Assert - Assert.Throws(() => new JsonOptions().EnsureConfigured()); - }, options); - } - - [ConditionalFact] - [RemoteExecutionSupported] - public void Configure_Works_WhenEnsureJsonTrimmabilityTrue() - { - var options = new RemoteInvokeOptions(); - options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); - - using var remoteHandle = RemoteExecutor.Invoke(static () => - { - // Arrange - var options = new JsonOptions(); - options.JsonSerializerOptions.AddContext(); - - // Act - var jsonOptions = options.EnsureConfigured(); - - // Assert - var serializerOptions = options.JsonSerializerOptions; - Assert.NotNull(serializerOptions.TypeInfoResolver); - Assert.IsType(serializerOptions.TypeInfoResolver); - Assert.True(serializerOptions.IsReadOnly); - }, options); - } - - [ConditionalFact] - [RemoteExecutionSupported] - public void DefaultSerializerOptions_Works_WhenEnsureJsonTrimmabilityFalse() - { - var options = new RemoteInvokeOptions(); - options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); - - using var remoteHandle = RemoteExecutor.Invoke(static () => - { - // Act - var options = new JsonOptions().EnsureConfigured(); - - // Assert - var serializerOptions = options.JsonSerializerOptions; - Assert.NotNull(serializerOptions.TypeInfoResolver); - Assert.IsType(serializerOptions.TypeInfoResolver); - Assert.True(serializerOptions.IsReadOnly); - }, options); - } - - [ConditionalFact] - [RemoteExecutionSupported] - public void DefaultSerializerOptions_Combines_WhenEnsureJsonTrimmabilityFalse() - { - var options = new RemoteInvokeOptions(); - options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); - - using var remoteHandle = RemoteExecutor.Invoke(static () => - { - // Arrange - var options = new JsonOptions(); - options.JsonSerializerOptions.AddContext(); - - // Act - var jsonOptions = options.EnsureConfigured(); - - // Assert - var serializerOptions = options.JsonSerializerOptions; - Assert.NotNull(serializerOptions.TypeInfoResolver); - Assert.IsNotType(serializerOptions.TypeInfoResolver); - Assert.IsNotType(serializerOptions.TypeInfoResolver); - Assert.NotNull(serializerOptions.TypeInfoResolver.GetTypeInfo(typeof(string), serializerOptions)); - Assert.True(serializerOptions.IsReadOnly); - }, options); - } - - [JsonSerializable(typeof(object))] - private partial class JsonSerializerExtensionsTestsContext : JsonSerializerContext - { } -} From c251205fa7e951810525c0f529fecec475ca1f27 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 10 Feb 2023 13:54:15 -0800 Subject: [PATCH 37/48] Fix mvc --- .../DependencyInjection/MvcCoreServiceCollectionExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index a8178f76ca78..8a5b67a830e1 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -280,5 +280,6 @@ internal static void AddMvcCoreServices(IServiceCollection services) private static void ConfigureDefaultServices(IServiceCollection services) { services.AddRouting(); + services.AddDefaultHttpJsonOptions(); } } From c01dc5fae5c1cc40179461c9f48388f36d036662 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 15 Feb 2023 13:23:22 -0800 Subject: [PATCH 38/48] More clean up --- .../src/HttpRequestJsonExtensions.cs | 10 ++------ .../src/RequestDelegateFactory.cs | 2 +- ...> JsonSerializerOptionsExtensionsTests.cs} | 18 ++++++------- .../Http.Results/src/HttpResultsHelper.cs | 25 +++---------------- ...tCore.Http.Results.WarningSuppressions.xml | 12 +++++++++ .../Microsoft.AspNetCore.Http.Results.csproj | 8 +++++- .../src/Properties/ILLink.Substitutions.xml | 8 ++++++ .../src/Microsoft.AspNetCore.Routing.csproj | 1 - src/Shared/Json/JsonSerializerExtensions.cs | 22 ++++++++++++++++ .../Json/JsonSerializerOptionsExtensions.cs | 8 +----- .../RouteHandlers/ExecuteHandlerHelper.cs | 23 +---------------- 11 files changed, 65 insertions(+), 72 deletions(-) rename src/Http/Http.Extensions/test/{JsonOptionsTests.cs => JsonSerializerOptionsExtensionsTests.cs} (83%) create mode 100644 src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.WarningSuppressions.xml create mode 100644 src/Http/Http.Results/src/Properties/ILLink.Substitutions.xml diff --git a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs index 044ea546fd9f..9f13d45bb053 100644 --- a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs @@ -32,10 +32,7 @@ public static class HttpRequestJsonExtensions this HttpRequest request, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(request); - - var options = ResolveSerializerOptions(request.HttpContext); - return request.ReadFromJsonAsync(jsonTypeInfo: (JsonTypeInfo)options.GetTypeInfo(typeof(TValue)), cancellationToken); + return request.ReadFromJsonAsync(options: null, cancellationToken); } /// @@ -111,10 +108,7 @@ public static class HttpRequestJsonExtensions Type type, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(request); - - var options = ResolveSerializerOptions(request.HttpContext); - return request.ReadFromJsonAsync(jsonTypeInfo: options.GetTypeInfo(type), cancellationToken); + return request.ReadFromJsonAsync(type, options: null, cancellationToken); } /// diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index f3dfb18524dd..9bac40a394d8 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -2314,7 +2314,7 @@ private static Task WriteJsonResponseFast(HttpResponse response, T value, Jso private static Task WriteJsonResponse(HttpResponse response, T? value, JsonSerializerOptions options, JsonTypeInfo jsonTypeInfo) { - return ExecuteHandlerHelper.WriteJsonResponseAsync(response, value, options, jsonTypeInfo); + return jsonTypeInfo.WriteToResponseAsync(response, value, options); } private static NotSupportedException GetUnsupportedReturnTypeException(Type returnType) diff --git a/src/Http/Http.Extensions/test/JsonOptionsTests.cs b/src/Http/Http.Extensions/test/JsonSerializerOptionsExtensionsTests.cs similarity index 83% rename from src/Http/Http.Extensions/test/JsonOptionsTests.cs rename to src/Http/Http.Extensions/test/JsonSerializerOptionsExtensionsTests.cs index 7cd854ec5c7b..263fe957bdc8 100644 --- a/src/Http/Http.Extensions/test/JsonOptionsTests.cs +++ b/src/Http/Http.Extensions/test/JsonSerializerOptionsExtensionsTests.cs @@ -4,17 +4,16 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; -using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Testing; using Microsoft.DotNet.RemoteExecutor; namespace Microsoft.AspNetCore.Http.Extensions.Tests; -public partial class JsonOptionsTests +public partial class JsonSerializerOptionsExtensionsTests { [ConditionalFact] [RemoteExecutionSupported] - public void Configure_ThrowsForNullTypeInfoResolver_WhenEnsureJsonTrimmabilityTrue_AndMarkReadonly() + public void EnsureConfigured_ThrowsForNullTypeInfoResolver_WhenEnsureJsonTrimmabilityTrue_AndMarkReadonly() { var options = new RemoteInvokeOptions(); options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); @@ -27,7 +26,7 @@ public void Configure_ThrowsForNullTypeInfoResolver_WhenEnsureJsonTrimmabilityTr } [Fact] - public void Configure_MarkAsReadOnly_WhenRequested() + public void EnsureConfigured_MarkAsReadOnly_WhenRequested() { // Arrange var options = new JsonSerializerOptions(); @@ -41,7 +40,7 @@ public void Configure_MarkAsReadOnly_WhenRequested() [ConditionalFact] [RemoteExecutionSupported] - public void Configure_Works_WhenEnsureJsonTrimmabilityTrue() + public void EnsureConfigured_Works_WhenEnsureJsonTrimmabilityTrue() { var options = new RemoteInvokeOptions(); options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); @@ -64,7 +63,7 @@ public void Configure_Works_WhenEnsureJsonTrimmabilityTrue() [ConditionalFact] [RemoteExecutionSupported] - public void DefaultSerializerOptions_Works_WhenEnsureJsonTrimmabilityFalse() + public void EnsureConfigured_Works_WhenNullTypeInfoResolverAndEnsureJsonTrimmabilityFalse() { var options = new RemoteInvokeOptions(); options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); @@ -83,7 +82,7 @@ public void DefaultSerializerOptions_Works_WhenEnsureJsonTrimmabilityFalse() [ConditionalFact] [RemoteExecutionSupported] - public void DefaultSerializerOptions_Combines_WhenEnsureJsonTrimmabilityFalse() + public void EnsureConfigured_DoesNotCombine_WhenResolverAlreadySetAndEnsureJsonTrimmabilityFalse() { var options = new RemoteInvokeOptions(); options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); @@ -99,9 +98,8 @@ public void DefaultSerializerOptions_Combines_WhenEnsureJsonTrimmabilityFalse() // Assert Assert.NotNull(options.TypeInfoResolver); - Assert.IsNotType(options.TypeInfoResolver); - Assert.IsNotType(options.TypeInfoResolver); - Assert.NotNull(options.TypeInfoResolver.GetTypeInfo(typeof(string), options)); + Assert.IsType(options.TypeInfoResolver); + Assert.NotNull(options.TypeInfoResolver.GetTypeInfo(typeof(object), options)); Assert.False(options.IsReadOnly); }, options); } diff --git a/src/Http/Http.Results/src/HttpResultsHelper.cs b/src/Http/Http.Results/src/HttpResultsHelper.cs index c91e8767dcd4..04708ceeb84e 100644 --- a/src/Http/Http.Results/src/HttpResultsHelper.cs +++ b/src/Http/Http.Results/src/HttpResultsHelper.cs @@ -32,31 +32,12 @@ public static Task WriteResultAsJsonAsync( } jsonSerializerOptions ??= httpContext.RequestServices.GetService>()?.Value.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; - //TODO: EnsureConfigured + jsonSerializerOptions.EnsureConfigured(); var jsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(TValue)); - Type? runtimeType; - if (jsonTypeInfo.IsValid(runtimeType = value.GetType())) - { - Log.WritingResultAsJson(logger, jsonTypeInfo.Type.Name); - return httpContext.Response.WriteAsJsonAsync( - value, - jsonTypeInfo, - contentType: contentType); - } - - Log.WritingResultAsJson(logger, runtimeType.Name); - // Since we don't know the type's polymorphic characteristics - // our best option is use the runtime type, so, - // call WriteAsJsonAsync() with the runtime type to serialize the runtime type rather than the declared type - // and avoid source generators issues. - // https://github.com/dotnet/aspnetcore/issues/43894 - // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism - return httpContext.Response.WriteAsJsonAsync( - value, - jsonSerializerOptions.GetTypeInfo(runtimeType), - contentType: contentType); + Log.WritingResultAsJson(logger, jsonTypeInfo.Type.Name); + return jsonTypeInfo.WriteToResponseAsync(httpContext.Response, value, jsonSerializerOptions, contentType); } public static Task WriteResultAsContentAsync( diff --git a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.WarningSuppressions.xml b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.WarningSuppressions.xml new file mode 100644 index 000000000000..5b679610cc9b --- /dev/null +++ b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.WarningSuppressions.xml @@ -0,0 +1,12 @@ + + + + + ILLink + IL2026 + member + M:Microsoft.AspNetCore.Http.JsonSerializerOptionsExtensions.EnsureConfigured(System.Text.Json.JsonSerializerOptions,System.Boolean) + This warning is left in the product so developers get an ILLink warning when trimming an app only when Microsoft.AspNetCore.EnsureJsonTrimmability=false. + + + \ No newline at end of file diff --git a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj index bf5308c62aad..61ca78358eeb 100644 --- a/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj +++ b/src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj @@ -19,10 +19,16 @@ - + + + + + + + diff --git a/src/Http/Http.Results/src/Properties/ILLink.Substitutions.xml b/src/Http/Http.Results/src/Properties/ILLink.Substitutions.xml new file mode 100644 index 000000000000..a7a53b456dde --- /dev/null +++ b/src/Http/Http.Results/src/Properties/ILLink.Substitutions.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index b4f551ae05df..2c06a5ae8854 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -30,7 +30,6 @@ - diff --git a/src/Shared/Json/JsonSerializerExtensions.cs b/src/Shared/Json/JsonSerializerExtensions.cs index 53e0229d3ada..20bff393e170 100644 --- a/src/Shared/Json/JsonSerializerExtensions.cs +++ b/src/Shared/Json/JsonSerializerExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -17,4 +18,25 @@ public static bool IsValid(this JsonTypeInfo jsonTypeInfo, [NotNullWhen(false)] public static JsonTypeInfo GetRequiredTypeInfo(this JsonSerializerContext context, Type type) => context.GetTypeInfo(type) ?? throw new InvalidOperationException($"Unable to obtain the JsonTypeInfo for type '{type.FullName}' from the context '{context.GetType().FullName}'."); + + public static Task WriteToResponseAsync(this JsonTypeInfo jsonTypeInfo, HttpResponse response, T? value, JsonSerializerOptions options, string? contentType = null) + { + var runtimeType = value?.GetType(); + + if (jsonTypeInfo.IsValid(runtimeType)) + { + // In this case the polymorphism is not + // relevant for us and will be handled by STJ, if needed. + return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value!, jsonTypeInfo, contentType: contentType, default); + } + + // Since we don't know the type's polymorphic characteristics + // our best option is use the runtime type, so, + // call WriteAsJsonAsync() with the runtime type to serialize the runtime type rather than the declared type + // and avoid source generators issues. + // https://github.com/dotnet/aspnetcore/issues/43894 + // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism + var runtimeTypeInfo = options.GetTypeInfo(runtimeType); + return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value!, runtimeTypeInfo, contentType: contentType, default); + } } diff --git a/src/Shared/Json/JsonSerializerOptionsExtensions.cs b/src/Shared/Json/JsonSerializerOptionsExtensions.cs index a95d8e78104e..6511e0d21174 100644 --- a/src/Shared/Json/JsonSerializerOptionsExtensions.cs +++ b/src/Shared/Json/JsonSerializerOptionsExtensions.cs @@ -16,13 +16,7 @@ internal static class JsonSerializerOptionsExtensions [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or ensure Microsoft.AspNetCore.EnsureJsonTrimmability=true for native AOT applications.")] public static void InitializeForReflection(this JsonSerializerOptions options) { - _defaultJsonTypeInfoResolver ??= new DefaultJsonTypeInfoResolver(); - - options.TypeInfoResolver = options.TypeInfoResolver switch - { - null => _defaultJsonTypeInfoResolver, - _ => JsonTypeInfoResolver.Combine(options.TypeInfoResolver, _defaultJsonTypeInfoResolver), - }; + options.TypeInfoResolver ??= _defaultJsonTypeInfoResolver ??= new DefaultJsonTypeInfoResolver(); } public static JsonSerializerOptions EnsureConfigured(this JsonSerializerOptions options, bool markAsReadOnly = false) diff --git a/src/Shared/RouteHandlers/ExecuteHandlerHelper.cs b/src/Shared/RouteHandlers/ExecuteHandlerHelper.cs index f2f0d657bfba..9087ca0f47ed 100644 --- a/src/Shared/RouteHandlers/ExecuteHandlerHelper.cs +++ b/src/Shared/RouteHandlers/ExecuteHandlerHelper.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; -using System.Text.Json.Serialization.Metadata; using System.Text.Json; namespace Microsoft.AspNetCore.Internal; @@ -24,8 +23,7 @@ public static Task ExecuteReturnAsync(object obj, HttpContext httpContext, JsonS else { // Otherwise, we JSON serialize when we reach the terminal state - var runtimeType = obj.GetType() ?? typeof(object); - return HttpResponseJsonExtensions.WriteAsJsonAsync(httpContext.Response, obj, runtimeType, options, default); + return HttpResponseJsonExtensions.WriteAsJsonAsync(httpContext.Response, obj, obj.GetType() ?? typeof(object), options, default); } } @@ -33,23 +31,4 @@ public static void SetPlaintextContentType(HttpContext httpContext) { httpContext.Response.ContentType ??= "text/plain; charset=utf-8"; } - - public static Task WriteJsonResponseAsync(HttpResponse response, T? value, JsonSerializerOptions options, JsonTypeInfo jsonTypeInfo) - { - var runtimeType = value?.GetType(); - - if (jsonTypeInfo.IsValid(runtimeType)) - { - // In this case the polymorphism is not - // relevant for us and will be handled by STJ, if needed. - return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value!, jsonTypeInfo, default); - } - - // Call WriteAsJsonAsync() with the runtime type to serialize the runtime type rather than the declared type - // and avoid source generators issues. - // https://github.com/dotnet/aspnetcore/issues/43894 - // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism - var runtimeTypeInfo = options.GetTypeInfo(runtimeType); - return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value!, runtimeTypeInfo, default); - } } From 9ceb581a752ca75878363a7b0ca3b6be70c65a92 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 15 Feb 2023 15:05:31 -0800 Subject: [PATCH 39/48] More clean up --- ...lidationProblemDetailsJsonConverterTest.cs | 5 +--- .../test/RequestDelegateFactoryTests.cs | 2 -- .../test/AcceptedAtRouteOfTResultTests.cs | 1 - .../test/AcceptedOfTResultTests.cs | 1 - .../test/HttpResultsHelperTests.cs | 24 ++++--------------- .../test/FunctionalTests/RouteHandlerTest.cs | 1 - 6 files changed, 5 insertions(+), 29 deletions(-) diff --git a/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs b/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs index 73b9c71aa600..b23565c2ef6f 100644 --- a/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs +++ b/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs @@ -10,10 +10,7 @@ namespace Microsoft.AspNetCore.Http.Abstractions.Tests; public class HttpValidationProblemDetailsJsonConverterTest { - private static JsonSerializerOptions JsonSerializerOptions => new JsonSerializerOptions(new JsonOptions().SerializerOptions) - { - TypeInfoResolver = new DefaultJsonTypeInfoResolver() - }; + private static JsonSerializerOptions JsonSerializerOptions => JsonOptions.DefaultSerializerOptions; [Fact] public void Write_Works() diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 076eff7fe460..32be764ad774 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -1643,7 +1643,6 @@ public async Task BindAsyncRunsBeforeBodyBinding() httpContext.Features.Set(new RequestBodyDetectionFeature(true)); var jsonOptions = new JsonOptions(); - jsonOptions.SerializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); jsonOptions.SerializerOptions.Converters.Add(new TodoJsonConverter()); var mock = new Mock(); @@ -1882,7 +1881,6 @@ public async Task RequestDelegatePopulatesFromBodyParameter(Delegate action) httpContext.Features.Set(new RequestBodyDetectionFeature(true)); var jsonOptions = new JsonOptions(); - jsonOptions.SerializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); jsonOptions.SerializerOptions.Converters.Add(new TodoJsonConverter()); var mock = new Mock(); diff --git a/src/Http/Http.Results/test/AcceptedAtRouteOfTResultTests.cs b/src/Http/Http.Results/test/AcceptedAtRouteOfTResultTests.cs index ab48d9ad14f7..a4b085a27f9f 100644 --- a/src/Http/Http.Results/test/AcceptedAtRouteOfTResultTests.cs +++ b/src/Http/Http.Results/test/AcceptedAtRouteOfTResultTests.cs @@ -220,7 +220,6 @@ private static IServiceProvider CreateServices(LinkGenerator linkGenerator) var services = new ServiceCollection(); services.AddLogging(); services.AddSingleton(linkGenerator); - services.AddDefaultHttpJsonOptions(); return services.BuildServiceProvider(); } } diff --git a/src/Http/Http.Results/test/AcceptedOfTResultTests.cs b/src/Http/Http.Results/test/AcceptedOfTResultTests.cs index 63e5934f5d13..669eba8d529c 100644 --- a/src/Http/Http.Results/test/AcceptedOfTResultTests.cs +++ b/src/Http/Http.Results/test/AcceptedOfTResultTests.cs @@ -144,7 +144,6 @@ private static IServiceProvider CreateServices() { var services = new ServiceCollection(); services.AddLogging(); - services.AddDefaultHttpJsonOptions(); return services.BuildServiceProvider(); } } diff --git a/src/Http/Http.Results/test/HttpResultsHelperTests.cs b/src/Http/Http.Results/test/HttpResultsHelperTests.cs index 5b5d883d6de7..6b2158d40c8e 100644 --- a/src/Http/Http.Results/test/HttpResultsHelperTests.cs +++ b/src/Http/Http.Results/test/HttpResultsHelperTests.cs @@ -27,16 +27,12 @@ public async Task WriteResultAsJsonAsync_Works_ForValueTypes(bool useJsonContext }; var responseBodyStream = new MemoryStream(); var httpContext = CreateHttpContext(responseBodyStream); - var serializerOptions = new JsonOptions().SerializerOptions; + var serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); if (useJsonContext) { serializerOptions.AddContext(); } - else - { - serializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); - } // Act await HttpResultsHelper.WriteResultAsJsonAsync(httpContext, NullLogger.Instance, value, jsonSerializerOptions: serializerOptions); @@ -62,16 +58,12 @@ public async Task WriteResultAsJsonAsync_Works_ForReferenceTypes(bool useJsonCon }; var responseBodyStream = new MemoryStream(); var httpContext = CreateHttpContext(responseBodyStream); - var serializerOptions = new JsonOptions().SerializerOptions; + var serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); if (useJsonContext) { serializerOptions.AddContext(); } - else - { - serializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); - } // Act await HttpResultsHelper.WriteResultAsJsonAsync(httpContext, NullLogger.Instance, value, jsonSerializerOptions: serializerOptions); @@ -99,16 +91,12 @@ public async Task WriteResultAsJsonAsync_Works_ForChildTypes(bool useJsonContext }; var responseBodyStream = new MemoryStream(); var httpContext = CreateHttpContext(responseBodyStream); - var serializerOptions = new JsonOptions().SerializerOptions; + var serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); if (useJsonContext) { serializerOptions.AddContext(); } - else - { - serializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); - } // Act await HttpResultsHelper.WriteResultAsJsonAsync(httpContext, NullLogger.Instance, value, jsonSerializerOptions: serializerOptions); @@ -137,16 +125,12 @@ public async Task WriteResultAsJsonAsync_Works_UsingBaseType_ForChildTypes(bool }; var responseBodyStream = new MemoryStream(); var httpContext = CreateHttpContext(responseBodyStream); - var serializerOptions = new JsonOptions().SerializerOptions; + var serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); if (useJsonContext) { serializerOptions.AddContext(); } - else - { - serializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); - } // Act await HttpResultsHelper.WriteResultAsJsonAsync(httpContext, NullLogger.Instance, value, jsonSerializerOptions: serializerOptions); diff --git a/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs b/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs index b03a1342ed6e..74e5568ffd9d 100644 --- a/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs +++ b/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs @@ -37,7 +37,6 @@ public async Task MapPost_FromBodyWorksWithJsonPayload() .ConfigureServices(services => { services.AddRouting(); - services.AddDefaultHttpJsonOptions(); }) .Build(); From 5bfb9e1331b5afe017c8300d6150daf2be248615 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 15 Feb 2023 15:25:00 -0800 Subject: [PATCH 40/48] Cleaning up JsonHttpResult --- src/Http/Http.Results/src/JsonHttpResultOfT.cs | 8 -------- .../Http.Results/src/JsonHttpResultTrimmerWarning.cs | 11 ----------- src/Http/Http.Results/src/Results.cs | 4 ---- src/Http/Http.Results/src/TypedResults.cs | 2 -- 4 files changed, 25 deletions(-) delete mode 100644 src/Http/Http.Results/src/JsonHttpResultTrimmerWarning.cs diff --git a/src/Http/Http.Results/src/JsonHttpResultOfT.cs b/src/Http/Http.Results/src/JsonHttpResultOfT.cs index 4841e675ecfd..5e2509b53acd 100644 --- a/src/Http/Http.Results/src/JsonHttpResultOfT.cs +++ b/src/Http/Http.Results/src/JsonHttpResultOfT.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Mvc; @@ -22,8 +21,6 @@ public sealed partial class JsonHttpResult : IResult, IStatusCodeHttpRes /// The serializer settings. /// The HTTP status code of the response. /// The value for the Content-Type header - [RequiresDynamicCode(JsonHttpResultTrimmerWarning.SerializationRequiresDynamicCodeMessage)] - [RequiresUnreferencedCode(JsonHttpResultTrimmerWarning.SerializationUnreferencedCodeMessage)] internal JsonHttpResult(TValue? value, JsonSerializerOptions? jsonSerializerOptions, int? statusCode = null, string? contentType = null) { Value = value; @@ -36,11 +33,6 @@ internal JsonHttpResult(TValue? value, JsonSerializerOptions? jsonSerializerOpti } StatusCode = statusCode; - if (jsonSerializerOptions is not null && !jsonSerializerOptions.IsReadOnly) - { - jsonSerializerOptions.TypeInfoResolver ??= new DefaultJsonTypeInfoResolver(); - } - JsonSerializerOptions = jsonSerializerOptions; } diff --git a/src/Http/Http.Results/src/JsonHttpResultTrimmerWarning.cs b/src/Http/Http.Results/src/JsonHttpResultTrimmerWarning.cs deleted file mode 100644 index e2120087ff24..000000000000 --- a/src/Http/Http.Results/src/JsonHttpResultTrimmerWarning.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Http; - -internal class JsonHttpResultTrimmerWarning -{ - public const string SerializationUnreferencedCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext."; - public const string SerializationRequiresDynamicCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use the overload that takes a JsonTypeInfo or JsonSerializerContext."; - -} diff --git a/src/Http/Http.Results/src/Results.cs b/src/Http/Http.Results/src/Results.cs index ab760c99f429..a2f5b78b6e13 100644 --- a/src/Http/Http.Results/src/Results.cs +++ b/src/Http/Http.Results/src/Results.cs @@ -183,8 +183,6 @@ public static IResult Content(string? content, MediaTypeHeaderValue contentType) /// as JSON format for the response. /// Callers should cache an instance of serializer settings to avoid /// recreating cached data with each call. - [RequiresUnreferencedCode(JsonHttpResultTrimmerWarning.SerializationUnreferencedCodeMessage)] - [RequiresDynamicCode(JsonHttpResultTrimmerWarning.SerializationRequiresDynamicCodeMessage)] public static IResult Json(object? data, JsonSerializerOptions? options = null, string? contentType = null, int? statusCode = null) => Json(data, options, contentType, statusCode); @@ -239,8 +237,6 @@ public static IResult Json(object? data, Type type, JsonSerializerContext contex /// as JSON format for the response. /// Callers should cache an instance of serializer settings to avoid /// recreating cached data with each call. - [RequiresUnreferencedCode(JsonHttpResultTrimmerWarning.SerializationUnreferencedCodeMessage)] - [RequiresDynamicCode(JsonHttpResultTrimmerWarning.SerializationRequiresDynamicCodeMessage)] #pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static IResult Json(TValue? data, JsonSerializerOptions? options = null, string? contentType = null, int? statusCode = null) #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters diff --git a/src/Http/Http.Results/src/TypedResults.cs b/src/Http/Http.Results/src/TypedResults.cs index ceb2cea29bb2..f78569f04bc1 100644 --- a/src/Http/Http.Results/src/TypedResults.cs +++ b/src/Http/Http.Results/src/TypedResults.cs @@ -193,8 +193,6 @@ public static ContentHttpResult Content(string? content, MediaTypeHeaderValue co /// The status code to set on the response. /// The created that serializes the specified /// as JSON format for the response. - [RequiresUnreferencedCode(JsonHttpResultTrimmerWarning.SerializationUnreferencedCodeMessage)] - [RequiresDynamicCode(JsonHttpResultTrimmerWarning.SerializationRequiresDynamicCodeMessage)] public static JsonHttpResult Json(TValue? data, JsonSerializerOptions? options = null, string? contentType = null, int? statusCode = null) => new(data, options, statusCode, contentType); From aaed7dce252a26b471a1ddb8f83caa1511c842be Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 15 Feb 2023 16:25:03 -0800 Subject: [PATCH 41/48] More clean up --- .../ValidationProblemDetailsJsonConverterTest.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Mvc/Mvc.Core/test/Infrastructure/ValidationProblemDetailsJsonConverterTest.cs b/src/Mvc/Mvc.Core/test/Infrastructure/ValidationProblemDetailsJsonConverterTest.cs index b88e37ba12a0..2f994f0af38d 100644 --- a/src/Mvc/Mvc.Core/test/Infrastructure/ValidationProblemDetailsJsonConverterTest.cs +++ b/src/Mvc/Mvc.Core/test/Infrastructure/ValidationProblemDetailsJsonConverterTest.cs @@ -3,16 +3,12 @@ using System.Text; using System.Text.Json; -using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Mvc.Infrastructure; public class ValidationProblemDetailsJsonConverterTest { - private static JsonSerializerOptions JsonSerializerOptions => new JsonSerializerOptions(new JsonOptions().JsonSerializerOptions) - { - TypeInfoResolver = new DefaultJsonTypeInfoResolver() - }; + private static JsonSerializerOptions JsonSerializerOptions => new JsonOptions().JsonSerializerOptions; [Fact] public void Read_Works() From 79d5117e1a38a922435128eabad2347d106accaf Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 16 Feb 2023 14:45:55 -0800 Subject: [PATCH 42/48] More clean up --- .../test/HttpValidationProblemDetailsJsonConverterTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs b/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs index b23565c2ef6f..48e73c457213 100644 --- a/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs +++ b/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs @@ -3,7 +3,6 @@ using System.Text; using System.Text.Json; -using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Http.Json; namespace Microsoft.AspNetCore.Http.Abstractions.Tests; From 7637bdd0b919a68deec0daf729e4bdfe6d028a0f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 16 Feb 2023 15:05:16 -0800 Subject: [PATCH 43/48] Moving changes to #46716 --- src/Http/Http.Extensions/src/JsonOptions.cs | 2 +- .../src/ProblemDetailsJsonOptionsSetup.cs | 20 ++++++------- ...mDetailsServiceCollectionExtensionsTest.cs | 29 ++++--------------- 3 files changed, 15 insertions(+), 36 deletions(-) diff --git a/src/Http/Http.Extensions/src/JsonOptions.cs b/src/Http/Http.Extensions/src/JsonOptions.cs index f29af9a5e77e..54d7860ec644 100644 --- a/src/Http/Http.Extensions/src/JsonOptions.cs +++ b/src/Http/Http.Extensions/src/JsonOptions.cs @@ -28,7 +28,7 @@ public class JsonOptions /// /// Gets the . /// - public JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions(_defaultSerializerOptions); + public JsonSerializerOptions SerializerOptions { get; internal set; } = new JsonSerializerOptions(_defaultSerializerOptions); /// /// Gets the default instance. diff --git a/src/Http/Http.Extensions/src/ProblemDetailsJsonOptionsSetup.cs b/src/Http/Http.Extensions/src/ProblemDetailsJsonOptionsSetup.cs index a574bd9e90da..b2fd02ba260e 100644 --- a/src/Http/Http.Extensions/src/ProblemDetailsJsonOptionsSetup.cs +++ b/src/Http/Http.Extensions/src/ProblemDetailsJsonOptionsSetup.cs @@ -1,5 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. + using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Http.Json; using Microsoft.Extensions.Options; @@ -10,18 +11,15 @@ internal sealed class ProblemDetailsJsonOptionsSetup : IPostConfigureOptions(); - break; - default: - break; + if (options.SerializerOptions.IsReadOnly) + { + options.SerializerOptions = new(options.SerializerOptions); + } + + // Combine the current resolver with our internal problem details context + options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(options.SerializerOptions.TypeInfoResolver!, ProblemDetailsJsonContext.Default); } } } diff --git a/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs index 7c4a591a044a..84c41a81ab09 100644 --- a/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs @@ -104,7 +104,7 @@ public void AddProblemDetails_CombinesProblemDetailsContext() } [Fact] - public void AddProblemDetails_Throws_ForReadOnlyJsonOptions() + public void AddProblemDetails_CombinesProblemDetailsContext_ForReadOnlyJsonOptions() { // Arrange var collection = new ServiceCollection(); @@ -121,7 +121,10 @@ public void AddProblemDetails_Throws_ForReadOnlyJsonOptions() var services = collection.BuildServiceProvider(); var jsonOptions = services.GetService>(); - Assert.Throws(() => jsonOptions.Value); + Assert.NotNull(jsonOptions.Value); + Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver); + Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver.GetTypeInfo(typeof(ProblemDetails), jsonOptions.Value.SerializerOptions)); + Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver.GetTypeInfo(typeof(TypeA), jsonOptions.Value.SerializerOptions)); } [Fact] @@ -164,28 +167,6 @@ public void AddProblemDetails_DoesNotCombineProblemDetailsContext_WhenNullTypeIn Assert.IsType(jsonOptions.Value.SerializerOptions.TypeInfoResolver); } - [Fact] - public void AddProblemDetails_CombineProblemDetailsContext_WhenDefaultypeInfoResolver() - { - // Arrange - var collection = new ServiceCollection(); - collection.AddOptions(); - collection.ConfigureAll(options => options.SerializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver()); - - // Act - collection.AddProblemDetails(); - - // Assert - var services = collection.BuildServiceProvider(); - var jsonOptions = services.GetService>(); - - Assert.NotNull(jsonOptions.Value); - Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver); - Assert.IsNotType(jsonOptions.Value.SerializerOptions.TypeInfoResolver); - Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver.GetTypeInfo(typeof(ProblemDetails), jsonOptions.Value.SerializerOptions)); - Assert.NotNull(jsonOptions.Value.SerializerOptions.TypeInfoResolver.GetTypeInfo(typeof(TypeA), jsonOptions.Value.SerializerOptions)); - } - [JsonSerializable(typeof(TypeA))] internal partial class TestExtensionsJsonContext : JsonSerializerContext { } From 14a5aac45fb32a7e69cd170563dba34328fdcab3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 17 Feb 2023 11:26:02 -0800 Subject: [PATCH 44/48] Fix comment --- src/Http/Http.Extensions/src/JsonOptions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/src/JsonOptions.cs b/src/Http/Http.Extensions/src/JsonOptions.cs index 54d7860ec644..f53fba6bbc1d 100644 --- a/src/Http/Http.Extensions/src/JsonOptions.cs +++ b/src/Http/Http.Extensions/src/JsonOptions.cs @@ -30,8 +30,9 @@ public class JsonOptions /// public JsonSerializerOptions SerializerOptions { get; internal set; } = new JsonSerializerOptions(_defaultSerializerOptions); + // Use a copy so the defaults are not modified. /// - /// Gets the default instance. + /// Gets the default read only instance. /// public static JsonSerializerOptions DefaultSerializerOptions { get => _defaultSerializerOptionsInstance ??= new JsonSerializerOptions(_defaultSerializerOptions).EnsureConfigured(markAsReadOnly: true); } From 35556daef5b396a2592a5b27de7160c4ae45730d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 24 Feb 2023 10:07:03 -0800 Subject: [PATCH 45/48] Removing public api --- src/DefaultBuilder/src/WebHost.cs | 1 - ...lidationProblemDetailsJsonConverterTest.cs | 2 +- .../src/HttpJsonOptionsFactory.cs | 44 ------------------- .../src/HttpJsonServiceExtensions.cs | 21 --------- src/Http/Http.Extensions/src/JsonOptions.cs | 14 ++---- ...oblemDetailsServiceCollectionExtensions.cs | 3 -- .../src/PublicAPI.Unshipped.txt | 2 - ...mDetailsServiceCollectionExtensionsTest.cs | 2 +- .../Http.Results/src/HttpResultsHelper.cs | 2 +- .../RequestDelegateFilterPipelineBuilder.cs | 2 +- .../DeveloperExceptionPageMiddlewareImpl.cs | 2 +- .../MvcCoreServiceCollectionExtensions.cs | 1 - .../Json/JsonSerializerOptionsExtensions.cs | 1 + 13 files changed, 9 insertions(+), 88 deletions(-) delete mode 100644 src/Http/Http.Extensions/src/HttpJsonOptionsFactory.cs diff --git a/src/DefaultBuilder/src/WebHost.cs b/src/DefaultBuilder/src/WebHost.cs index ba8900982e9c..4a49c3ef8e3c 100644 --- a/src/DefaultBuilder/src/WebHost.cs +++ b/src/DefaultBuilder/src/WebHost.cs @@ -256,7 +256,6 @@ internal static void UseKestrel(IWebHostBuilder builder) services.AddTransient(); services.AddTransient(); services.AddTransient, ForwardedHeadersOptionsSetup>(); - services.AddDefaultHttpJsonOptions(); services.AddRouting(); }); diff --git a/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs b/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs index 48e73c457213..b2c791d102bd 100644 --- a/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs +++ b/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Http.Abstractions.Tests; public class HttpValidationProblemDetailsJsonConverterTest { - private static JsonSerializerOptions JsonSerializerOptions => JsonOptions.DefaultSerializerOptions; + private static JsonSerializerOptions JsonSerializerOptions => new JsonOptions().SerializerOptions; [Fact] public void Write_Works() diff --git a/src/Http/Http.Extensions/src/HttpJsonOptionsFactory.cs b/src/Http/Http.Extensions/src/HttpJsonOptionsFactory.cs deleted file mode 100644 index 2149dc42b314..000000000000 --- a/src/Http/Http.Extensions/src/HttpJsonOptionsFactory.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Http.Json; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Http; - -internal sealed class HttpJsonOptionsFactory : IOptionsFactory -{ - private readonly OptionsFactory _innerOptionsFactory; - - /// - /// Initializes a new instance with the specified options configurations. - /// - /// The configuration actions to run. - /// The initialization actions to run. - public HttpJsonOptionsFactory(IEnumerable> setups, IEnumerable> postConfigures) - : this(setups, postConfigures, validations: Array.Empty>()) - { } - - /// - /// Initializes a new instance with the specified options configurations. - /// - /// The configuration actions to run. - /// The initialization actions to run. - /// The validations to run. - public HttpJsonOptionsFactory(IEnumerable> setups, IEnumerable> postConfigures, IEnumerable> validations) - { - _innerOptionsFactory = new OptionsFactory(setups, postConfigures, validations); - } - - public JsonOptions Create(string name) - { - var options = _innerOptionsFactory.Create(name); - - // After the options is completed created - // we need to ensure we have it configured for - // reflection (if needed) and marked as read-only - options.SerializerOptions.EnsureConfigured(markAsReadOnly: true); - - return options; - } -} diff --git a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs index 8b05b5db4d7d..4e4e6f58dbb7 100644 --- a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Json; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; @@ -25,27 +24,7 @@ public static class HttpJsonServiceExtensions /// The modified . public static IServiceCollection ConfigureHttpJsonOptions(this IServiceCollection services, Action configureOptions) { - services.AddDefaultHttpJsonOptions(); services.Configure(configureOptions); - - return services; - } - - /// - /// Configures the default options used for reading and writing JSON when using - /// - /// and . - /// uses default values from JsonSerializerDefaults.Web. - /// - /// The to configure options on. - /// The modified . - public static IServiceCollection AddDefaultHttpJsonOptions(this IServiceCollection services) - { - ArgumentNullException.ThrowIfNull(services); - - services.AddOptions(); - services.TryAddTransient, HttpJsonOptionsFactory>(); - return services; } } diff --git a/src/Http/Http.Extensions/src/JsonOptions.cs b/src/Http/Http.Extensions/src/JsonOptions.cs index f53fba6bbc1d..bfe9dc3703c2 100644 --- a/src/Http/Http.Extensions/src/JsonOptions.cs +++ b/src/Http/Http.Extensions/src/JsonOptions.cs @@ -14,26 +14,18 @@ namespace Microsoft.AspNetCore.Http.Json; /// public class JsonOptions { - private static JsonSerializerOptions? _defaultSerializerOptionsInstance; - private static readonly JsonSerializerOptions _defaultSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) + internal static readonly JsonSerializerOptions DefaultSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) { // Web defaults don't use the relex JSON escaping encoder. // // Because these options are for producing content that is written directly to the request // (and not embedded in an HTML page for example), we can use UnsafeRelaxedJsonEscaping. - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; // Use a copy so the defaults are not modified. /// /// Gets the . /// - public JsonSerializerOptions SerializerOptions { get; internal set; } = new JsonSerializerOptions(_defaultSerializerOptions); - - // Use a copy so the defaults are not modified. - /// - /// Gets the default read only instance. - /// - public static JsonSerializerOptions DefaultSerializerOptions { get => _defaultSerializerOptionsInstance ??= new JsonSerializerOptions(_defaultSerializerOptions).EnsureConfigured(markAsReadOnly: true); } - + public JsonSerializerOptions SerializerOptions { get; internal set; } = new JsonSerializerOptions(DefaultSerializerOptions); } diff --git a/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs b/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs index 1b38c7fdfc81..f76540a67838 100644 --- a/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs +++ b/src/Http/Http.Extensions/src/ProblemDetailsServiceCollectionExtensions.cs @@ -37,9 +37,6 @@ public static IServiceCollection AddProblemDetails( { ArgumentNullException.ThrowIfNull(services); - // Adding default HttpJsonOptions. - services.AddDefaultHttpJsonOptions(); - // Adding default services; services.TryAddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index d1db4b5a9ecb..2f0545145f3d 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,7 +1,5 @@ #nullable enable static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, object? value, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, string? contentType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.AspNetCore.Http.Json.JsonOptions.DefaultSerializerOptions.get -> System.Text.Json.JsonSerializerOptions! -static Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.AddDefaultHttpJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! Microsoft.AspNetCore.Mvc.ProblemDetails.Extensions.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) Microsoft.AspNetCore.Http.HttpValidationProblemDetails.Errors.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) diff --git a/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs index 84c41a81ab09..d3ef5b68e46c 100644 --- a/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs @@ -164,7 +164,7 @@ public void AddProblemDetails_DoesNotCombineProblemDetailsContext_WhenNullTypeIn var jsonOptions = services.GetService>(); Assert.NotNull(jsonOptions.Value); - Assert.IsType(jsonOptions.Value.SerializerOptions.TypeInfoResolver); + Assert.Null(jsonOptions.Value.SerializerOptions.TypeInfoResolver); } [JsonSerializable(typeof(TypeA))] diff --git a/src/Http/Http.Results/src/HttpResultsHelper.cs b/src/Http/Http.Results/src/HttpResultsHelper.cs index 04708ceeb84e..4d307fccb27b 100644 --- a/src/Http/Http.Results/src/HttpResultsHelper.cs +++ b/src/Http/Http.Results/src/HttpResultsHelper.cs @@ -31,7 +31,7 @@ public static Task WriteResultAsJsonAsync( return Task.CompletedTask; } - jsonSerializerOptions ??= httpContext.RequestServices.GetService>()?.Value.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; + jsonSerializerOptions ??= httpContext.RequestServices.GetService>()?.Value.SerializerOptions ?? new JsonOptions().SerializerOptions; jsonSerializerOptions.EnsureConfigured(); var jsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(TValue)); diff --git a/src/Http/Routing/src/RequestDelegateFilterPipelineBuilder.cs b/src/Http/Routing/src/RequestDelegateFilterPipelineBuilder.cs index d298bd2f315d..0873aa7186de 100644 --- a/src/Http/Routing/src/RequestDelegateFilterPipelineBuilder.cs +++ b/src/Http/Routing/src/RequestDelegateFilterPipelineBuilder.cs @@ -18,7 +18,7 @@ public static RequestDelegate Create(RequestDelegate requestDelegate, RequestDel Debug.Assert(options.EndpointBuilder != null); var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; - var jsonSerializerOptions = serviceProvider?.GetService>()?.Value.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; + var jsonSerializerOptions = serviceProvider?.GetService>()?.Value.SerializerOptions ?? new JsonOptions().SerializerOptions; var factoryContext = new EndpointFilterFactoryContext { diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs index 3431d06954ba..158a8ca9b954 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs @@ -84,7 +84,7 @@ public DeveloperExceptionPageMiddlewareImpl( private static ExtensionsExceptionJsonContext CreateSerializationContext(JsonOptions? jsonOptions) { // Create context from configured options to get settings such as PropertyNamePolicy and DictionaryKeyPolicy. - return new ExtensionsExceptionJsonContext(new JsonSerializerOptions(jsonOptions?.SerializerOptions ?? JsonOptions.DefaultSerializerOptions)); + return new ExtensionsExceptionJsonContext(new JsonSerializerOptions(jsonOptions?.SerializerOptions ?? new JsonOptions().SerializerOptions)); } /// diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 8a5b67a830e1..a8178f76ca78 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -280,6 +280,5 @@ internal static void AddMvcCoreServices(IServiceCollection services) private static void ConfigureDefaultServices(IServiceCollection services) { services.AddRouting(); - services.AddDefaultHttpJsonOptions(); } } diff --git a/src/Shared/Json/JsonSerializerOptionsExtensions.cs b/src/Shared/Json/JsonSerializerOptionsExtensions.cs index 6511e0d21174..f42a6dd37d9d 100644 --- a/src/Shared/Json/JsonSerializerOptionsExtensions.cs +++ b/src/Shared/Json/JsonSerializerOptionsExtensions.cs @@ -23,6 +23,7 @@ public static JsonSerializerOptions EnsureConfigured(this JsonSerializerOptions { if (!options.IsReadOnly) { + // This might be removed once https://github.com/dotnet/runtime/issues/80529 is fixed if (!TrimmingAppContextSwitches.EnsureJsonTrimmability) { #pragma warning disable IL2026 // Suppressed in Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml From 29fd8cd73e951e4534ee5178998c0b50ab453f24 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 24 Feb 2023 15:56:54 -0800 Subject: [PATCH 46/48] PR feedback updates --- .../src/HttpRequestJsonExtensions.cs | 76 ++++++++--------- .../src/HttpResponseJsonExtensions.cs | 84 +++++++++---------- .../JsonSerializerOptionsExtensionsTests.cs | 9 +- .../Json/JsonSerializerOptionsExtensions.cs | 4 +- 4 files changed, 85 insertions(+), 88 deletions(-) diff --git a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs index 9f13d45bb053..6b25faa71f74 100644 --- a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs @@ -95,6 +95,44 @@ public static class HttpRequestJsonExtensions } } + /// + /// Read JSON from the request and deserialize to object type. + /// If the request's content-type is not a known JSON type then an error will be thrown. + /// + /// The request to read from. + /// Metadata about the type to convert. + /// A used to cancel the operation. + /// The deserialized value. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static async ValueTask ReadFromJsonAsync( +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + this HttpRequest request, + JsonTypeInfo jsonTypeInfo, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + + if (!request.HasJsonContentType(out var charset)) + { + ThrowContentTypeError(request); + } + + var encoding = GetEncodingFromCharset(charset); + var (inputStream, usesTranscodingStream) = GetInputStream(request.HttpContext, encoding); + + try + { + return await JsonSerializer.DeserializeAsync(inputStream, jsonTypeInfo, cancellationToken); + } + finally + { + if (usesTranscodingStream) + { + await inputStream.DisposeAsync(); + } + } + } + /// /// Read JSON from the request and deserialize to the specified type. /// If the request's content-type is not a known JSON type then an error will be thrown. @@ -177,44 +215,6 @@ public static class HttpRequestJsonExtensions } } - /// - /// Read JSON from the request and deserialize to object type. - /// If the request's content-type is not a known JSON type then an error will be thrown. - /// - /// The request to read from. - /// Metadata about the type to convert. - /// A used to cancel the operation. - /// The deserialized value. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters - public static async ValueTask ReadFromJsonAsync( -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters - this HttpRequest request, - JsonTypeInfo jsonTypeInfo, - CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(request); - - if (!request.HasJsonContentType(out var charset)) - { - ThrowContentTypeError(request); - } - - var encoding = GetEncodingFromCharset(charset); - var (inputStream, usesTranscodingStream) = GetInputStream(request.HttpContext, encoding); - - try - { - return await JsonSerializer.DeserializeAsync(inputStream, jsonTypeInfo, cancellationToken); - } - finally - { - if (usesTranscodingStream) - { - await inputStream.DisposeAsync(); - } - } - } - /// /// Checks the Content-Type header for JSON types. /// diff --git a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs index 2442cee133af..7ab75c3afafc 100644 --- a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs @@ -119,6 +119,47 @@ static async Task WriteAsJsonAsyncSlow(HttpResponse response, TValue value, Json } } + /// + /// Write the specified value as JSON to the response body. The response content-type will be set to + /// the specified content-type. + /// + /// The response to write JSON to. + /// The value to write as JSON. + /// Metadata about the type to convert. + /// The content-type to set on the response. + /// A used to cancel the operation. + /// The task object representing the asynchronous operation. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static Task WriteAsJsonAsync( +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + this HttpResponse response, + object? value, + JsonTypeInfo jsonTypeInfo, + string? contentType = default, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(response); + + response.ContentType = contentType ?? JsonConstants.JsonContentTypeWithCharset; + + // if no user provided token, pass the RequestAborted token and ignore OperationCanceledException + if (!cancellationToken.CanBeCanceled) + { + return WriteAsJsonAsyncSlow(response, value, jsonTypeInfo); + } + + return JsonSerializer.SerializeAsync(response.Body, value, jsonTypeInfo, cancellationToken); + + static async Task WriteAsJsonAsyncSlow(HttpResponse response, object? value, JsonTypeInfo jsonTypeInfo) + { + try + { + await JsonSerializer.SerializeAsync(response.Body, value, jsonTypeInfo, response.HttpContext.RequestAborted); + } + catch (OperationCanceledException) { } + } + } + /// /// Write the specified value as JSON to the response body. The response content-type will be set to /// application/json; charset=utf-8. @@ -196,9 +237,7 @@ public static Task WriteAsJsonAsync( /// The content-type to set on the response. /// A used to cancel the operation. /// The task object representing the asynchronous operation. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters public static Task WriteAsJsonAsync( -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters this HttpResponse response, object? value, Type type, @@ -230,47 +269,6 @@ async Task WriteAsJsonAsyncSlow() } } - /// - /// Write the specified value as JSON to the response body. The response content-type will be set to - /// the specified content-type. - /// - /// The response to write JSON to. - /// The value to write as JSON. - /// Metadata about the type to convert. - /// The content-type to set on the response. - /// A used to cancel the operation. - /// The task object representing the asynchronous operation. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters - public static Task WriteAsJsonAsync( -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters - this HttpResponse response, - object? value, - JsonTypeInfo jsonTypeInfo, - string? contentType = default, - CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(response); - - response.ContentType = contentType ?? JsonConstants.JsonContentTypeWithCharset; - - // if no user provided token, pass the RequestAborted token and ignore OperationCanceledException - if (!cancellationToken.CanBeCanceled) - { - return WriteAsJsonAsyncSlow(response, value, jsonTypeInfo); - } - - return JsonSerializer.SerializeAsync(response.Body, value, jsonTypeInfo, cancellationToken); - - static async Task WriteAsJsonAsyncSlow(HttpResponse response, object? value, JsonTypeInfo jsonTypeInfo) - { - try - { - await JsonSerializer.SerializeAsync(response.Body, value, jsonTypeInfo, response.HttpContext.RequestAborted); - } - catch (OperationCanceledException) { } - } - } - private static JsonSerializerOptions ResolveSerializerOptions(HttpContext httpContext) { // Attempt to resolve options from DI then fallback to default options diff --git a/src/Http/Http.Extensions/test/JsonSerializerOptionsExtensionsTests.cs b/src/Http/Http.Extensions/test/JsonSerializerOptionsExtensionsTests.cs index 263fe957bdc8..5430c7d8f3d7 100644 --- a/src/Http/Http.Extensions/test/JsonSerializerOptionsExtensionsTests.cs +++ b/src/Http/Http.Extensions/test/JsonSerializerOptionsExtensionsTests.cs @@ -32,7 +32,7 @@ public void EnsureConfigured_MarkAsReadOnly_WhenRequested() var options = new JsonSerializerOptions(); // Act - _ = options.EnsureConfigured(markAsReadOnly: true); + options.EnsureConfigured(markAsReadOnly: true); // Assert Assert.True(options.IsReadOnly); @@ -52,7 +52,7 @@ public void EnsureConfigured_Works_WhenEnsureJsonTrimmabilityTrue() options.AddContext(); // Act - _ = options.EnsureConfigured(); + options.EnsureConfigured(); // Assert Assert.NotNull(options.TypeInfoResolver); @@ -71,7 +71,8 @@ public void EnsureConfigured_Works_WhenNullTypeInfoResolverAndEnsureJsonTrimmabi using var remoteHandle = RemoteExecutor.Invoke(static () => { // Act - var options = new JsonSerializerOptions().EnsureConfigured(); + var options = new JsonSerializerOptions(); + options.EnsureConfigured(); // Assert Assert.NotNull(options.TypeInfoResolver); @@ -94,7 +95,7 @@ public void EnsureConfigured_DoesNotCombine_WhenResolverAlreadySetAndEnsureJsonT options.AddContext(); // Act - _ = options.EnsureConfigured(); + options.EnsureConfigured(); // Assert Assert.NotNull(options.TypeInfoResolver); diff --git a/src/Shared/Json/JsonSerializerOptionsExtensions.cs b/src/Shared/Json/JsonSerializerOptionsExtensions.cs index f42a6dd37d9d..5d516e825824 100644 --- a/src/Shared/Json/JsonSerializerOptionsExtensions.cs +++ b/src/Shared/Json/JsonSerializerOptionsExtensions.cs @@ -19,7 +19,7 @@ public static void InitializeForReflection(this JsonSerializerOptions options) options.TypeInfoResolver ??= _defaultJsonTypeInfoResolver ??= new DefaultJsonTypeInfoResolver(); } - public static JsonSerializerOptions EnsureConfigured(this JsonSerializerOptions options, bool markAsReadOnly = false) + public static void EnsureConfigured(this JsonSerializerOptions options, bool markAsReadOnly = false) { if (!options.IsReadOnly) { @@ -38,7 +38,5 @@ public static JsonSerializerOptions EnsureConfigured(this JsonSerializerOptions options.MakeReadOnly(); } } - - return options; } } From 8c830d1580a15b342d01ac2f35f6f44afc404d10 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 24 Feb 2023 15:58:04 -0800 Subject: [PATCH 47/48] Removing extra usings --- src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs index 4e4e6f58dbb7..d4990c174922 100644 --- a/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpJsonServiceExtensions.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http.Json; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection; From 2c5caffedf81ee4862adbcbb7f920f2c61d8822d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 28 Feb 2023 10:43:30 -0800 Subject: [PATCH 48/48] Fix tests --- src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs b/src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs index 25b0f5220604..5169ccd6442c 100644 --- a/src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs +++ b/src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs @@ -55,11 +55,10 @@ public ValueTask WriteAsync(ProblemDetailsContext context) ProblemDetailsDefaults.Apply(context.ProblemDetails, httpContext.Response.StatusCode); _options.CustomizeProblemDetails?.Invoke(context); - var problemDetailsType = context.ProblemDetails.GetType(); - return new ValueTask(httpContext.Response.WriteAsJsonAsync( context.ProblemDetails, - _serializerOptions.GetTypeInfo(problemDetailsType), + context.ProblemDetails.GetType(), + _serializerOptions, contentType: "application/problem+json")); } }