Abp Blazor WebAssembly - 使用 System.Text.Json 的多态 DTO 反序列化

Abp Blazor WebAssembly - Polymorphic DTO Deserialization using System.Text.Json

Abp 框架版本:5.0.0-beta2,UI:Blazor WebAssembly

我正在尝试在 ABP 框架内实现多态性,以便能够在 API 后端和 Blazor WebAssembly 前端之间交换派生的 classes,但我无法让 Blazor反序列化 JSON 多态:

// Output Dtos
public abstract class AnimalOutputDto : EntityDto<Guid>
{
  public string Name { get; set; }
}

public class CatOutputDto : AnimalOutputDto
{
  public string Name { get; set; }
  public string Color { get; set; }
}

// Input Dtos
public abstract class AnimalInputDto : EntityDto<Guid>
{
  public string Name { get; set; }
}

public class CatInputDto : AnimalInputDto
{
  public string Name { get; set; }
  public string Color { get; set; }
}

当将模型从 Blazor 前端传递到 HTTP API 时,我能够使用本文中描述的自定义 JsonConverter 正确反序列化它们,我将其添加到 HTTPAPI项目然后在HTTPAPI.Host项目的ConfigureServices方法中引用:

https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-6-0#support-polymorphic-deserialization

        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            var configuration = context.Services.GetConfiguration();
            var hostingEnvironment = context.Services.GetHostingEnvironment();
            ..Usual configuration statements..
            ConfigureJsonConverters(context);
        }

        private void ConfigureJsonConverters(ServiceConfigurationContext context)
        {
            context.Services.AddControllers(options =>
            {
            }).AddJsonOptions(options => {
                options.JsonSerializerOptions.Converters.Add(new AnimalJsonConverter());
            });                
        }

当模型传回 Blazor 前端时,我可以验证它是否使用正确的转换器序列化,该类型也在 Microsoft 文章中指定。

但是,当 Blazor 接收到模型时,抛出异常:它显然没有识别多态类型,而是试图反序列化抽象基 class:

Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported.

似乎我需要找到一种方法来在 Blazor 项目中注册相同的自定义 JSON 转换器 classes,就像在 HttpApi.Host 项目中所做的那样。但是,我找不到有关如何完成此操作的任何文档。

有人对此有任何信息或指导吗?

使用 System.Text.Json 仍有一些限制 - 请看这里:https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to#table-of-differences-between-newtonsoftjson-and-systemtextjson

虽然它有一个解决方法,但多态序列化和反序列化似乎是其中之一。

我认为你只能在 Blazor 端使用 Newtonsoft.Json

Always Use the Newtonsoft.Json

If you want to continue to use the Newtonsoft.Json library for all the types, you can set UseHybridSerializer to false in the PreConfigureServices method of your module class:

PreConfigure<AbpJsonOptions>(options =>
{
    options.UseHybridSerializer = false;
});

参考文献:

  1. Deserialization of reference types without parameterless constructor is not supported
  2. https://docs.abp.io/en/abp/latest/JSON#abpjsonoptions
  3. https://docs.abp.io/en/abp/4.4/Migration-Guides/Abp-4_0#unsupported-types

我设法通过使用 JsonConvert class 和 [JsonConverter] 属性来完成这项工作。这种方式在 ConfigureServices() 方法中不需要配置。

  1. 向我的 .Application.Contracts 项目添加了输入和输出 DTO,并仅在 BASE 类 上用 [JsonConverter(typeof(MyConverterClass))] 属性装饰它们(添加这个子属性 class 似乎会在序列化程序中导致循环。)

  2. 添加了一个枚举 属性,它覆盖了基础 class,从而表示派生的 class 类型,用作鉴别器

  3. 创建了一个合适的转换器class(在与 DTO 相同的项目中)

DTO classes:

    [JsonConvert(typeof(AnimalInputJsonConverter))]
    public abstract class AnimalInputDto : EntityDto<Guid>
    {
        public string Name { get; set; }    
        public virtual AnimalType AnimalType => AnimalType.NotSelected
    } 

    public class CatInputDto : AnimalInputDto
    {
        public override AnimalType AnimalType => AnimalType.Cat
        [.. more properties specific to Cat]
    }

    [JsonConvert(typeof(AnimalOutputJsonConverter))]
    public abstract class AnimalOutputDto : EntityDto<Guid>
    {
        public string Name { get; set; }    
        public virtual AnimalType AnimalType => AnimalType.NotSelected
    } 

    public class CatOutputDto : AnimalOutputDto
    {
        public override AnimalType AnimalType => AnimalType.Cat
        [.. more properties specific to Cat]
    }

转换器示例(代码在输入和输出 DTO 之间基本相同)

    public class AnimalInputDtoJsonConverter : JsonConverter<AnimalInputDto>
    {
        public override bool CanConvert(Type typeToConvert) =>
            typeof(AnimalInputDto).IsAssignableFrom(typeToConvert);

        public override AnimalInputDto Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            // Take a copy of the reader as we need to check through the object first before deserializing.
            Utf8JsonReader readerClone = reader;

            if (readerClone.TokenType != JsonTokenType.StartObject)
            {
                throw new JsonException();
            }

            AnimalType typeDiscriminator = AnimalType.NotSelected;
            string camelCasedPropertyName = 
                nameof(AnimalDto.AnimalType).ToCamelCase();

            // Loop through the JSON tokens. Look for the required property by name.
            while (readerClone.Read())
            {
                if (readerClone.TokenType == JsonTokenType.PropertyName && readerClone.GetString() == camelCasedPropertyName)
                {
                    // Move on to the value, which has to parse out to an enum
                    readerClone.Read();
                    if (readerClone.TokenType == JsonTokenType.Number)
                    {
                        int value = readerClone.GetInt32();
                        try 
                        {
                            typeDiscriminator = (AnimalType)value;
                            break;
                        }
                        catch (InvalidCastException)
                        {
                            throw new JsonException($"{value} is not a recognised integer representation of {typeof(AnimalType)}");
                        }
                    }
                }
            }

            AnimalInputDto target = typeDiscriminator switch
            {
                AnimalType.Cat => JsonSerializer.Deserialize<CatInputDto>(ref reader, options),
                _ => throw new NotSupportedException($"The supplied object is not a recognised derivative of {typeof(AnimalInputDto)}")
            };

            return target;
        }

        public override void Write(
            Utf8JsonWriter writer,
            AnimalInputDto value,
            JsonSerializerOptions options)
        {
            JsonSerializer.Serialize(writer, value, value.GetType(), options);
        }
    }

此外,通用方法似乎是可能的,尽管此代码未经过优化或性能测试,但我预计使用 Activator.CreateInstance() 检查其鉴别器值的反射和对象实例化会导致性能下降.

请注意,下面假设鉴别器 属性 是一个枚举,并且派生的 class 具有与枚举类型完全相同的 属性 名称:

用法如下:

    [JsonConvert(typeof(PolymorphicJsonConverter<AnimalInputDto, AnimalType>))]
    public abstract class AnimalInputDto : EntityDto<Guid>
    {
        public string Name { get; set; }    
        public virtual AnimalType AnimalType => AnimalType.NotSelected
    } 

    ...

    public class PolymorphicJsonConverter<T, U> : JsonConverter<T>
        where T : EntityDto<Guid>
        where U : Enum
    {
        public string TypeDiscriminator { get; private set; }
        public string TypeDiscriminatorCamelCase { get; private set; }

        public List<Type> DerivableTypes { get; private set; }

        public PolymorphicJsonConverter()
            : base()
        {
            TypeDiscriminator = typeof(U).Name;
            TypeDiscriminatorCamelCase = TypeDiscriminator.ToCamelCase();
            DerivableTypes = new List<Type>();
            foreach (var domainAssembly in AppDomain.CurrentDomain.GetAssemblies())
            {
                var assemblyTypes = domainAssembly.GetTypes()
                  .Where(type => type.IsSubclassOf(typeof(T)) && !type.IsAbstract);

                DerivableTypes.AddRange(assemblyTypes);
            }
        }

        public override bool CanConvert(Type typeToConvert) =>
            typeof(T).IsAssignableFrom(typeToConvert);

        public override T Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            // Take a copy of the reader as we need to check through the object first before deserializing.
            Utf8JsonReader readerClone = reader;

            if (readerClone.TokenType != JsonTokenType.StartObject)
            {
                throw new JsonException();
            }

            U typeDiscriminatorValue = (U)Enum.ToObject(typeof(U), 0);

            // Loop through the JSON tokens. Look for the required property by name.
            while (readerClone.Read())
            {
                if (readerClone.TokenType == JsonTokenType.PropertyName && readerClone.GetString() == TypeDiscriminatorCamelCase)
                {
                    // Move on to the value, which has to parse out to an enum
                    readerClone.Read();
                    if (readerClone.TokenType == JsonTokenType.Number)
                    {
                        int value = readerClone.GetInt32();
                        try
                        {
                            typeDiscriminatorValue = (U)Enum.ToObject(typeof(U), value);
                            break;
                        }
                        catch (InvalidCastException)
                        {
                            throw new NotSupportedException($"{value} is not a recognised integer representation of {typeof(U)}");
                        }
                    }
                }
            }

            T target = null;

            foreach(var dt in DerivableTypes)
            {
                var newInst = Activator.CreateInstance(dt);
                var propValue = (U)newInst.GetType().GetProperty(TypeDiscriminator).GetValue(newInst, null);
                if (propValue.Equals(typeDiscriminatorValue))
                {
                    target = (T)JsonSerializer.Deserialize(ref reader, dt, options);
                }
            }

            if (target == null)
            {
                throw new NotSupportedException($"The supplied object is not a recognised derivative of {typeof(T)}");
            }

            return target;
        }

        public override void Write(
            Utf8JsonWriter writer,
            T value,
            JsonSerializerOptions options)
        {
            JsonSerializer.Serialize(writer, value, value.GetType(), options);
        }

    }

以上灵感/进一步阅读: https://getyourbitstogether.com/polymorphic-serialization-in-system-text-json/ https://vpaulino.wordpress.com/2021/02/23/deserializing-polymorphic-types-with-system-text-json/ https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-6-0 https://docs.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonconverter-1?view=net-6.0 https://docs.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonconverterattribute?view=net-6.0