File: Internal\Json\MessageTypeInfoResolver.cs
Web Access
Project: src\src\Grpc\JsonTranscoding\src\Microsoft.AspNetCore.Grpc.JsonTranscoding\Microsoft.AspNetCore.Grpc.JsonTranscoding.csproj (Microsoft.AspNetCore.Grpc.JsonTranscoding)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Google.Protobuf;
using Google.Protobuf.Reflection;
using Grpc.Shared;
using Type = System.Type;
 
namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
 
internal sealed class MessageTypeInfoResolver : IJsonTypeInfoResolver
{
    private readonly JsonContext _context;
 
    public MessageTypeInfoResolver(JsonContext context)
    {
        _context = context;
    }
 
    public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
    {
        if (!IsStandardMessage(type, out var messageDescriptor))
        {
            return null;
        }
 
        var typeInfo = JsonTypeInfo.CreateJsonTypeInfo(type, options);
        typeInfo.CreateObject = () => Activator.CreateInstance(type)!;
 
        var fields = messageDescriptor.Fields.InFieldNumberOrder();
 
        // The field map can have multiple entries for a property:
        // 1. The JSON field name, e.g. firstName. This is used to serialize and deserialize JSON.
        // 2. The original field name, e.g. first_name. This might be different. It is only used to deserialize JSON.
        var mappings = CreateJsonFieldMap(fields);
 
        foreach (var field in fields)
        {
            var propertyInfo = CreatePropertyInfo(typeInfo, field.JsonName, field, isSerializable: true);
            typeInfo.Properties.Add(propertyInfo);
 
            // We have a property for reading and writing the JSON name so remove from mappings.
            mappings.Remove(field.JsonName);
        }
 
        // Fields have two mappings: the original field name and the camelcased JSON name.
        // The JSON name can also be customized in proto with json_name option.
        // Remaining mappings are for extra setter only properties.
        foreach (var mapping in mappings)
        {
            var propertyInfo = CreatePropertyInfo(typeInfo, mapping.Key, mapping.Value, isSerializable: false);
            typeInfo.Properties.Add(propertyInfo);
        }
 
        return typeInfo;
    }
 
    private bool IsStandardMessage(Type type, [NotNullWhen(true)] out MessageDescriptor? messageDescriptor)
    {
        if (type.IsInterface || !typeof(IMessage).IsAssignableFrom(type))
        {
            messageDescriptor = null;
            return false;
        }
 
        messageDescriptor = (MessageDescriptor?) _context.DescriptorRegistry.FindDescriptorByType(type);
        if (messageDescriptor == null)
        {
            throw new InvalidOperationException("Couldn't resolve descriptor for message type: " + type);
        }
 
        // Wrappers and well known types are handled by converters.
        if (ServiceDescriptorHelpers.IsWrapperType(messageDescriptor))
        {
            return false;
        }
        if (JsonConverterHelper.WellKnownTypeNames.ContainsKey(messageDescriptor.FullName))
        {
            return false;
        }
 
        return true;
    }
 
    private JsonPropertyInfo CreatePropertyInfo(JsonTypeInfo typeInfo, string name, FieldDescriptor field, bool isSerializable)
    {
        var propertyInfo = typeInfo.CreateJsonPropertyInfo(
            JsonConverterHelper.GetFieldType(field),
            name);
 
        propertyInfo.ShouldSerialize = (o, v) =>
        {
            // Properties that don't have this flag set are only used to deserialize incoming JSON.
            if (!isSerializable)
            {
                return false;
            }
            return JsonConverterHelper.ShouldFormatFieldValue((IMessage)o, field, v, !_context.Settings.IgnoreDefaultValues);
        };
        propertyInfo.Get = (o) =>
        {
            return field.Accessor.GetValue((IMessage)o);
        };
 
        if (field.IsMap || field.IsRepeated)
        {
            // Collection properties are read-only. Populate values into existing collection.
            propertyInfo.ObjectCreationHandling = JsonObjectCreationHandling.Populate;
        }
        else
        {
            propertyInfo.Set = GetSetMethod(field);
        }
 
        return propertyInfo;
    }
 
    private static Action<object, object?> GetSetMethod(FieldDescriptor field)
    {
        Debug.Assert(!field.IsRepeated && !field.IsMap, "Collections shouldn't have a setter.");
 
        if (field.RealContainingOneof != null)
        {
            return (o, v) =>
            {
                var caseField = field.RealContainingOneof.Accessor.GetCaseFieldDescriptor((IMessage)o);
                if (caseField != null)
                {
                    throw new InvalidOperationException($"Multiple values specified for oneof {field.RealContainingOneof.Name}.");
                }
 
                SetFieldValue(field, (IMessage)o, v);
            };
        }
 
        return (o, v) =>
        {
            SetFieldValue(field, (IMessage)o, v);
        };
 
        static void SetFieldValue(FieldDescriptor field, IMessage m, object? v)
        {
            if (v != null)
            {
                field.Accessor.SetValue(m, v);
            }
            else
            {
                field.Accessor.Clear(m);
            }
        }
    }
 
    private static Dictionary<string, FieldDescriptor> CreateJsonFieldMap(IList<FieldDescriptor> fields)
    {
        var map = new Dictionary<string, FieldDescriptor>();
        // The ordering is important here: JsonName takes priority over Name,
        // which means we need to put JsonName values in the map after *all*
        // Name keys have been added. See https://github.com/protocolbuffers/protobuf/issues/11987
        foreach (var field in fields)
        {
            map[field.Name] = field;
        }
        foreach (var field in fields)
        {
            map[field.JsonName] = field;
        }
        return map;
    }
}