File: Internal\DefaultJsonSerializerFactory.cs
Web Access
Project: src\src\Libraries\Microsoft.Extensions.Caching.Hybrid\Microsoft.Extensions.Caching.Hybrid.csproj (Microsoft.Extensions.Caching.Hybrid)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
 
namespace Microsoft.Extensions.Caching.Hybrid.Internal;
 
internal sealed class DefaultJsonSerializerFactory : IHybridCacheSerializerFactory
{
    private readonly IServiceProvider _serviceProvider;
 
    internal static JsonSerializerOptions FieldEnabledJsonOptions { get; } = new() { IncludeFields = true };
 
    internal JsonSerializerOptions Options { get; }
 
    public DefaultJsonSerializerFactory(IServiceProvider serviceProvider)
    {
        // store the service provider and obtain the default JSON options, keyed by the **open** generic interface type
        _serviceProvider = serviceProvider;
 
        Options = serviceProvider.GetKeyedService<JsonSerializerOptions>(typeof(IHybridCacheSerializer<>)) ?? SystemDefaultJsonOptions;
    }
 
    public bool TryCreateSerializer<T>([NotNullWhen(true)] out IHybridCacheSerializer<T>? serializer)
    {
        // no restriction - accept any type (i.e. always return true)
 
        // see if there is a per-type options registered (keyed by the **closed** generic type), otherwise use the default
        JsonSerializerOptions options = _serviceProvider.GetKeyedService<JsonSerializerOptions>(typeof(IHybridCacheSerializer<T>)) ?? Options;
        if (!options.IncludeFields && ReferenceEquals(options, SystemDefaultJsonOptions) && IsFieldOnlyType(typeof(T)))
        {
            // value-tuples expose fields, not properties; special-case this as a common scenario
            options = FieldEnabledJsonOptions;
        }
 
        serializer = new DefaultJsonSerializer<T>(options);
        return true;
    }
 
    internal static bool IsFieldOnlyType(Type type)
    {
        Dictionary<Type, FieldOnlyResult>? state = null; // only needed for complex types
        return IsFieldOnlyType(type, ref state) == FieldOnlyResult.FieldOnly;
    }
 
#pragma warning disable IDE0079 // unnecessary suppression: TFM-dependent
#pragma warning disable IL2026, IL3050 // AOT bits
    private static JsonSerializerOptions SystemDefaultJsonOptions => 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")]
    [SuppressMessage("Performance", "CA1864:Prefer the 'IDictionary.TryAdd(TKey, TValue)' method", Justification = "Not available in all platforms")]
    private static FieldOnlyResult IsFieldOnlyType(
        Type type, ref Dictionary<Type, FieldOnlyResult>? state)
    {
        if (type is null || type.IsPrimitive || type == typeof(string))
        {
            return FieldOnlyResult.NotFieldOnly;
        }
 
        // re-use existing results, and more importantly: prevent infinite recursion
        if (state is not null && state.TryGetValue(type, out var existingResult))
        {
            return existingResult;
        }
 
        // check for collection types; start at IEnumerable and then look for IEnumerable<T>
        // (this is broadly comparable to STJ)
        if (typeof(IEnumerable).IsAssignableFrom(type))
        {
            PrepareStateForDepth(type, ref state);
            foreach (var iType in type.GetInterfaces())
            {
                if (iType.IsGenericType && iType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
                {
                    if (IsFieldOnlyType(iType.GetGenericArguments()[0], ref state) == FieldOnlyResult.FieldOnly)
                    {
                        return SetState(type, state, true);
                    }
                }
            }
 
            // no problems detected
            return SetState(type, state, false);
        }
 
        // not a collection; check for field-only scenario - look for properties first
        var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
        if (props.Length != 0)
        {
            PrepareStateForDepth(type, ref state);
            foreach (var prop in props)
            {
                if (IsFieldOnlyType(prop.PropertyType, ref state) == FieldOnlyResult.FieldOnly)
                {
                    return SetState(type, state, true);
                }
            }
 
            // then we *do* have public instance properties, that aren't themselves problems; we're good
            return SetState(type, state, false);
        }
 
        // no properties; if there are fields, this is the problem scenario we're trying to detect
        var haveFields = type.GetFields(BindingFlags.Public | BindingFlags.Instance).Length != 0;
        return SetState(type, state, haveFields);
 
        static void PrepareStateForDepth(Type type, ref Dictionary<Type, FieldOnlyResult>? state)
        {
            state ??= [];
            if (!state.ContainsKey(type))
            {
                state.Add(type, FieldOnlyResult.Incomplete);
            }
        }
 
        static FieldOnlyResult SetState(Type type, Dictionary<Type, FieldOnlyResult>? state, bool result)
        {
            var value = result ? FieldOnlyResult.FieldOnly : FieldOnlyResult.NotFieldOnly;
            if (state is not null)
            {
                state[type] = value;
            }
 
            return value;
        }
    }
 
    internal sealed class DefaultJsonSerializer<T> : IHybridCacheSerializer<T>
    {
        internal JsonSerializerOptions Options { get; }
 
        public DefaultJsonSerializer(JsonSerializerOptions options)
        {
            Options = options;
        }
 
#pragma warning disable IDE0079 // unnecessary suppression: TFM-dependent
#pragma warning disable IL2026, IL3050 // AOT bits
        T IHybridCacheSerializer<T>.Deserialize(ReadOnlySequence<byte> source)
        {
            var reader = new Utf8JsonReader(source);
            return JsonSerializer.Deserialize<T>(ref reader, Options)!;
 
        }
 
        void IHybridCacheSerializer<T>.Serialize(T value, IBufferWriter<byte> target)
        {
            using var writer = new Utf8JsonWriter(target);
 
            JsonSerializer.Serialize<T>(writer, value, Options);
        }
#pragma warning restore IL2026, IL3050
#pragma warning restore IDE0079
    }
 
    // used to store intermediate state when calculating IsFieldOnlyType
    private enum FieldOnlyResult
    {
        Incomplete = 0,
        FieldOnly = 1,
        NotFieldOnly = 2,
    }
}