From 118bdf9357ad6c0d19841e27be5dae6449990c5b Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 21 May 2025 11:02:32 +0100 Subject: [PATCH 1/4] 1. don't touch JsonSerializerOptions.Default in AOT mode 2. add new extension methods to register JsonSerializerOptions 3. offer a suitable error message instead, if none supplied in AOT mode open question: naming: With* or Add* ? --- .../HybridCacheBuilderExtensions.cs | 22 +++++++++++ .../Internal/DefaultJsonSerializerFactory.cs | 39 ++++++++++++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheBuilderExtensions.cs index d8fa3a3a3ad..e184bb8b9a3 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheBuilderExtensions.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 Microsoft.Extensions.Caching.Hybrid; using Microsoft.Shared.Diagnostics; @@ -59,4 +60,25 @@ public static IHybridCacheBuilder AddSerializerFactory< _ = Throw.IfNull(builder).Services.AddSingleton(); return builder; } + + /// + /// Register a default for use with JSON serialization. + /// + /// The instance. + public static IHybridCacheBuilder WithJsonSerializerOptions(this IHybridCacheBuilder builder, JsonSerializerOptions options) + { + _ = Throw.IfNull(builder).Services.AddKeyedSingleton(typeof(IHybridCacheSerializer<>), Throw.IfNull(options)); + return builder; + } + + /// + /// Register a for use with JSON serialization of type . + /// + /// The type being serialized. + /// The instance. + public static IHybridCacheBuilder WithJsonSerializerOptions(this IHybridCacheBuilder builder, JsonSerializerOptions options) + { + _ = Throw.IfNull(builder).Services.AddKeyedSingleton(typeof(IHybridCacheSerializer), Throw.IfNull(options)); + return builder; + } } diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultJsonSerializerFactory.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultJsonSerializerFactory.cs index f499ba485b3..e338f635f23 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultJsonSerializerFactory.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultJsonSerializerFactory.cs @@ -10,6 +10,10 @@ using System.Text.Json; using Microsoft.Extensions.DependencyInjection; +[assembly: UnconditionalSuppressMessage("AOT", "IL2026", Justification = "Checked at runtime, guidance issued")] +[assembly: UnconditionalSuppressMessage("AOT", "IL2070", Justification = "Checked at runtime, guidance issued")] +[assembly: UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Checked at runtime, guidance issued")] + namespace Microsoft.Extensions.Caching.Hybrid.Internal; internal sealed class DefaultJsonSerializerFactory : IHybridCacheSerializerFactory @@ -34,7 +38,7 @@ public bool TryCreateSerializer([NotNullWhen(true)] out IHybridCacheSerialize // see if there is a per-type options registered (keyed by the **closed** generic type), otherwise use the default JsonSerializerOptions options = _serviceProvider.GetKeyedService(typeof(IHybridCacheSerializer)) ?? Options; - if (!options.IncludeFields && ReferenceEquals(options, SystemDefaultJsonOptions) && IsFieldOnlyType(typeof(T))) + if (!options.IncludeFields && IsDefaultJsonOptions(options) && IsFieldOnlyType(typeof(T))) { // value-tuples expose fields, not properties; special-case this as a common scenario options = FieldEnabledJsonOptions; @@ -50,11 +54,42 @@ internal static bool IsFieldOnlyType(Type type) return IsFieldOnlyType(type, ref state) == FieldOnlyResult.FieldOnly; } + private static bool IsDefaultJsonOptions(JsonSerializerOptions options) + { +#if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + if (!System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) + { + // can't be, since we don't use default options for AOT + return false; + } +#endif + +#pragma warning disable IDE0079 // unnecessary suppression: TFM-dependent +#pragma warning disable IL2026, IL3050 // AOT bits + return ReferenceEquals(options, JsonSerializerOptions.Default); +#pragma warning restore IL2026, IL3050 +#pragma warning restore IDE0079 + } + + private static JsonSerializerOptions SystemDefaultJsonOptions + { + get + { +#if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + if (!System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) + { + throw new NotSupportedException($"When using AOT, {nameof(JsonSerializerOptions)} with {nameof(JsonSerializerOptions.TypeInfoResolver)} specified must be provided via" + + $" {nameof(IHybridCacheBuilder)}.{nameof(HybridCacheBuilderExtensions.WithJsonSerializerOptions)}."); + } +#endif + #pragma warning disable IDE0079 // unnecessary suppression: TFM-dependent #pragma warning disable IL2026, IL3050 // AOT bits - private static JsonSerializerOptions SystemDefaultJsonOptions => JsonSerializerOptions.Default; + return JsonSerializerOptions.Default; #pragma warning restore IL2026, IL3050 #pragma warning restore IDE0079 + } + } [SuppressMessage("Trimming", "IL2070:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The parameter of method does not have matching annotations.", Justification = "Custom serializers may be needed for AOT with STJ")] From 4b6267335b7e8369e0d9f771a95ade7f33d82028 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 21 May 2025 11:25:31 +0100 Subject: [PATCH 2/4] Use the new API in the tests --- .../SerializerTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/SerializerTests.cs b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/SerializerTests.cs index 5153fc643a7..0d0b734c6e8 100644 --- a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/SerializerTests.cs +++ b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/SerializerTests.cs @@ -236,20 +236,20 @@ public class NodeB private static T RoundTrip(T value, ReadOnlySpan expectedBytes, JsonSerializer expectedJsonOptions, JsonSerializer addSerializers = JsonSerializer.None, bool binary = false) { var services = new ServiceCollection(); - services.AddHybridCache(); + var hc = services.AddHybridCache(); JsonSerializerOptions? globalOptions = null; JsonSerializerOptions? perTypeOptions = null; if ((addSerializers & JsonSerializer.CustomGlobal) != JsonSerializer.None) { globalOptions = new() { IncludeFields = true }; // assume any custom options will serialize the whole type - services.AddKeyedSingleton(typeof(IHybridCacheSerializer<>), globalOptions); + hc.WithJsonSerializerOptions(globalOptions); } if ((addSerializers & JsonSerializer.CustomPerType) != JsonSerializer.None) { perTypeOptions = new() { IncludeFields = true }; // assume any custom options will serialize the whole type - services.AddKeyedSingleton(typeof(IHybridCacheSerializer), perTypeOptions); + hc.WithJsonSerializerOptions(perTypeOptions); } JsonSerializerOptions? expectedOptionsObj = expectedJsonOptions switch From b5eb531bde80b27427814aa3e776cbe0e56625bc Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 21 May 2025 12:18:15 +0100 Subject: [PATCH 3/4] mark new APIs as [Experimental] --- .../HybridCacheBuilderExtensions.cs | 3 +++ src/Shared/DiagnosticIds/DiagnosticIds.cs | 1 + 2 files changed, 4 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheBuilderExtensions.cs index e184bb8b9a3..6977bdf0e21 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheBuilderExtensions.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.DependencyInjection; @@ -65,6 +66,7 @@ public static IHybridCacheBuilder AddSerializerFactory< /// Register a default for use with JSON serialization. /// /// The instance. + [Experimental(DiagnosticIds.Experiments.HybridCache, UrlFormat = DiagnosticIds.UrlFormat)] public static IHybridCacheBuilder WithJsonSerializerOptions(this IHybridCacheBuilder builder, JsonSerializerOptions options) { _ = Throw.IfNull(builder).Services.AddKeyedSingleton(typeof(IHybridCacheSerializer<>), Throw.IfNull(options)); @@ -76,6 +78,7 @@ public static IHybridCacheBuilder WithJsonSerializerOptions(this IHybridCacheBui /// /// The type being serialized. /// The instance. + [Experimental(DiagnosticIds.Experiments.HybridCache, UrlFormat = DiagnosticIds.UrlFormat)] public static IHybridCacheBuilder WithJsonSerializerOptions(this IHybridCacheBuilder builder, JsonSerializerOptions options) { _ = Throw.IfNull(builder).Services.AddKeyedSingleton(typeof(IHybridCacheSerializer), Throw.IfNull(options)); diff --git a/src/Shared/DiagnosticIds/DiagnosticIds.cs b/src/Shared/DiagnosticIds/DiagnosticIds.cs index 0fafcb46879..0c8093a6bfb 100644 --- a/src/Shared/DiagnosticIds/DiagnosticIds.cs +++ b/src/Shared/DiagnosticIds/DiagnosticIds.cs @@ -45,6 +45,7 @@ internal static class Experiments internal const string DocumentDb = "EXTEXP0011"; internal const string AutoActivation = "EXTEXP0012"; internal const string HttpLogging = "EXTEXP0013"; + internal const string HybridCache = "EXTEXP0018"; } internal static class LoggerMessage From 7d862c57706190cbda8f0f8f39ca51218235c971 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 22 May 2025 09:16:53 +0100 Subject: [PATCH 4/4] PR feedback --- .../Internal/DefaultJsonSerializerFactory.cs | 15 +++++---------- .../Microsoft.Extensions.Caching.Hybrid.csproj | 5 +++++ ...oft.Extensions.AotCompatibility.TestApp.csproj | 2 -- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultJsonSerializerFactory.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultJsonSerializerFactory.cs index e338f635f23..f1c6ea5278d 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultJsonSerializerFactory.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultJsonSerializerFactory.cs @@ -10,12 +10,11 @@ using System.Text.Json; using Microsoft.Extensions.DependencyInjection; -[assembly: UnconditionalSuppressMessage("AOT", "IL2026", Justification = "Checked at runtime, guidance issued")] -[assembly: UnconditionalSuppressMessage("AOT", "IL2070", Justification = "Checked at runtime, guidance issued")] -[assembly: UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Checked at runtime, guidance issued")] - namespace Microsoft.Extensions.Caching.Hybrid.Internal; +[UnconditionalSuppressMessage("AOT", "IL2026", Justification = "Checked at runtime, guidance issued")] +[UnconditionalSuppressMessage("AOT", "IL2070", Justification = "Checked at runtime, guidance issued")] +[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Checked at runtime, guidance issued")] internal sealed class DefaultJsonSerializerFactory : IHybridCacheSerializerFactory { private readonly IServiceProvider _serviceProvider; @@ -56,13 +55,11 @@ internal static bool IsFieldOnlyType(Type type) private static bool IsDefaultJsonOptions(JsonSerializerOptions options) { -#if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER - if (!System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) + if (!JsonSerializer.IsReflectionEnabledByDefault) { // can't be, since we don't use default options for AOT return false; } -#endif #pragma warning disable IDE0079 // unnecessary suppression: TFM-dependent #pragma warning disable IL2026, IL3050 // AOT bits @@ -75,13 +72,11 @@ private static JsonSerializerOptions SystemDefaultJsonOptions { get { -#if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER - if (!System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) + if (!JsonSerializer.IsReflectionEnabledByDefault) { throw new NotSupportedException($"When using AOT, {nameof(JsonSerializerOptions)} with {nameof(JsonSerializerOptions.TypeInfoResolver)} specified must be provided via" + $" {nameof(IHybridCacheBuilder)}.{nameof(HybridCacheBuilderExtensions.WithJsonSerializerOptions)}."); } -#endif #pragma warning disable IDE0079 // unnecessary suppression: TFM-dependent #pragma warning disable IL2026, IL3050 // AOT bits diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Microsoft.Extensions.Caching.Hybrid.csproj b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Microsoft.Extensions.Caching.Hybrid.csproj index 6c96c7bac38..3c596e14fb6 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Microsoft.Extensions.Caching.Hybrid.csproj +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Microsoft.Extensions.Caching.Hybrid.csproj @@ -23,6 +23,11 @@ false + + true + true + + normal 82 diff --git a/test/Libraries/Microsoft.Extensions.AotCompatibility.TestApp/Microsoft.Extensions.AotCompatibility.TestApp.csproj b/test/Libraries/Microsoft.Extensions.AotCompatibility.TestApp/Microsoft.Extensions.AotCompatibility.TestApp.csproj index 770832e8a29..13f00e7be7c 100644 --- a/test/Libraries/Microsoft.Extensions.AotCompatibility.TestApp/Microsoft.Extensions.AotCompatibility.TestApp.csproj +++ b/test/Libraries/Microsoft.Extensions.AotCompatibility.TestApp/Microsoft.Extensions.AotCompatibility.TestApp.csproj @@ -17,8 +17,6 @@ - -