【问题标题】:Abp Blazor WebAssembly - Polymorphic DTO Deserialization using System.Text.JsonAbp Blazor WebAssembly - 使用 System.Text.Json 的多态 DTO 反序列化
【发布时间】:2021-11-19 09:43:00
【问题描述】:

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

我正在尝试在 ABP 框架内实现多态性,以便能够在 API 后端和 Blazor WebAssembly 前端之间交换派生类,但我无法让 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 项目中,然后在的 ConfigureServices 方法中引用HTTPAPI.Host 项目:

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 接收到模型时,会抛出异常:它显然无法识别多态类型,而是试图反序列化抽象基类:

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

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

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

【问题讨论】:

    标签: polymorphism blazor-webassembly abp


    【解决方案1】:

    使用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

    始终使用 Newtonsoft.Json

    如果您想继续使用 Newtonsoft.Json 库进行所有 类型,您可以将UseHybridSerializer 设置为 false PreConfigureServices 模块类的方法:

    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

    【讨论】:

    • 感谢您的回复和 ABP 文档的链接。但是,当我尝试实现 ABP 文档(从 IJsonSerializerProvider 继承)中详述的转换器时,ABP 似乎没有选择我的转换器;在调试它们中的断点时,它们永远不会被命中。我在 ConfigureServices() 方法的每个模块中这样注册它们: Configure(options => { options.Providers.Insert(0, typeof(AnimalJsonConverter)); });跨度>
    • 我更新了答案。
    • 所以我已经尝试了所有这些选项,但我无法识别 ABP JSON 转换器,无论是从 Blazor 入站到后端还是反之亦然: - 创建 IJsonSerializerProvider 实例(AnimalDtoJsonConverter) -在模块的 PreConfigure() 方法中注册(也在 ConfigureServices() 中尝试过) - 尝试在位置 0 插入(而不是 Add())但没有区别 - 尝试将混合序列化程序设置为 false (并尝试将基本 DTO 类设置为不支持)无论我做什么,我都无法让 ABP 识别并点击序列化程序..
    • 很抱歉,但幸运的是,我可以从您添加的答案中看到问题已经解决,我很高兴。我将通过将UseHybridSerializer 设置为false 来测试多态序列化和反序列化。
    • 谢谢。请注意,我的回答不涉及恢复到 Newtonsoft JSON,而是使用 System.Text.Json。
    【解决方案2】:

    我设法通过使用 JsonConvert 类和 [JsonConverter] 属性来完成这项工作。这样ConfigureServices()方法就不需要配置了。

    1. 在我的 .Application.Contracts 项目中添加了输入和输出 DTO,并在 BASE CLASSES 上使用 [JsonConverter(typeof(MyConverterClass))] 属性进行了修饰(将此属性添加到子类将导致序列化程序中出现循环好像。)

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

    3. 在以下行中创建了一个适当的转换器类(与 DTO 在同一项目中)

    DTO 类:

        [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() 来检查其鉴别器的值的对象实例化会降低性能。

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

    如下使用:

        [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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-12-08
      • 2021-12-30
      • 2021-07-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多