diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheBuilderExtensions.cs index d8fa3a3a3ad..6977bdf0e21 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheBuilderExtensions.cs @@ -2,7 +2,9 @@ // 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.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.DependencyInjection; @@ -59,4 +61,27 @@ public static IHybridCacheBuilder AddSerializerFactory< _ = Throw.IfNull(builder).Services.AddSingleton(); return builder; } + + /// + /// 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)); + return builder; + } + + /// + /// Register a for use with JSON serialization of type . + /// + /// 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)); + 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..f1c6ea5278d 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultJsonSerializerFactory.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultJsonSerializerFactory.cs @@ -12,6 +12,9 @@ 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; @@ -34,7 +37,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 +53,38 @@ internal static bool IsFieldOnlyType(Type type) return IsFieldOnlyType(type, ref state) == FieldOnlyResult.FieldOnly; } + private static bool IsDefaultJsonOptions(JsonSerializerOptions options) + { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + // can't be, since we don't use default options for AOT + return false; + } + #pragma warning disable IDE0079 // unnecessary suppression: TFM-dependent #pragma warning disable IL2026, IL3050 // AOT bits - private static JsonSerializerOptions SystemDefaultJsonOptions => JsonSerializerOptions.Default; + return ReferenceEquals(options, JsonSerializerOptions.Default); #pragma warning restore IL2026, IL3050 #pragma warning restore IDE0079 + } + + private static JsonSerializerOptions SystemDefaultJsonOptions + { + get + { + 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)}."); + } + +#pragma warning disable IDE0079 // unnecessary suppression: TFM-dependent +#pragma warning disable IL2026, IL3050 // AOT bits + 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")] 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/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 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 @@ - - 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