首页 > 其他分享 >Abp Blazor WebAssembly - Polymorphic DTO Deserialization using System.Text.Json

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

时间:2023-07-17 16:12:29浏览次数:64  
标签:WebAssembly AnimalType DTO Deserialization share public options data class

@@abp 4.0 dto jobject

 

https://stackoverflow.com/questions/70032776/abp-blazor-webassembly-polymorphic-dto-deserialization-using-system-text-json

1

Abp Framework version: 5.0.0-beta2, UI: Blazor WebAssembly

I'm attempting to implement polymorphism within the ABP framework to be able to exchange derived classes between the API backend and the Blazor WebAssembly front end, and am having trouble getting Blazor to deserialize the JSON polymorphically:

// 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; }
}

When passing models from the Blazor front-end to the HTTP API, I am able to correctly deserialize them by using a custom JsonConverter as described in this article, which I added to the HTTPAPI project and then referenced in the ConfigureServices method of the HTTPAPI.Host project:

https://learn.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());
            });                
        }

When the model is passed back to the Blazor front-end I can verify that it is being serialized using the correct converter for the type also specified in the Microsoft article.

However, when the model is received by Blazor, an exception is thrown: it is clearly not recognising the polymorphic type, and instead is trying to deserialize the abstract base class:

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

It seems as if I would need to find a way to register the same custom JSON converter classes in the Blazor project as done in the HttpApi.Host project. However I cannot find any documentation on how this is done.

Does anybody have any information or guidance on this?

Share Improve this question   edited Nov 19, 2021 at 10:25     asked Nov 19, 2021 at 9:43 GDUnit's user avatar GDUnit 9322 silver badges88 bronze badges Add a comment

2 Answers

1

There are still some limitations using System.Text.Json - have a look here: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to#table-of-differences-between-newtonsoftjson-and-systemtextjson

Although it has a workaround, Polymorphic serialization and deserialization seem to be one of them.

I think you can only use Newtonsoft.Json on Blazor side.

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;
});

References:

  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
Share Improve this answer   edited Nov 19, 2021 at 13:50     answered Nov 19, 2021 at 10:16 berkansasmaz's user avatar berkansasmaz 59844 silver badges1616 bronze badges
  • 1 Thanks for the reply and the link to the ABP document. However, when I try to implement the converters as detailed in the ABP docs (inherit from IJsonSerializerProvider) ABP does not seem to pick up my converters; when debugging the breakpoints within them are never hit. I am registering them like this in each module within the ConfigureServices() method: Configure<Volo.Abp.Json.AbpJsonOptions>(options => { options.Providers.Insert(0, typeof(AnimalJsonConverter)); });  – GDUnit  Nov 19, 2021 at 13:25 
  •   So I've tried all these options and I can't get the ABP JSON converters to be recognised, either inbound from Blazor to the backend or vice versa: - Creating instance of IJsonSerializerProvider (AnimalDtoJsonConverter) - Registering in PreConfigure() method of module (also tried in ConfigureServices()) - Tried inserting at position 0 (instead of Add()) but no difference - Tried setting hybrid serializer to false (and also tried setting the base DTO class as not supported) Whatever I do, I cannot get ABP to recognise and hit the serializer..  – GDUnit  Nov 19, 2021 at 17:36 
  •   I'm sorry, but fortunately, I can see from the answer you added that the issue has been resolved, I'm glad about that. I'll test Polymorphic serialization and deserialization by setting UseHybridSerializer to false.  – berkansasmaz  Nov 22, 2021 at 11:38 
  • 1 Thanks. Note that my answer did not involve reverting to the Newtonsoft JSON, but rather using System.Text.Json.  – GDUnit  Nov 23, 2021 at 7:03
Add a comment <iframe data-google-container-id="4" data-is-safeframe="true" data-load-complete="true" frameborder="0" height="90" id="google_ads_iframe_/248424177/stackoverflow.com/mlb/question-pages_0" marginheight="0" marginwidth="0" scrolling="no" src="https://983f406331032203740d66b47e4d968a.safeframe.googlesyndication.com/safeframe/1-0-40/html/container.html" title="3rd party ad content" width="728"></iframe> 1  

I managed to make this work by using the JsonConvert class and [JsonConverter] attribute. There is no configuration necessary in the ConfigureServices() method this way.

  1. Added input and output DTOs to my .Application.Contracts project, and decorated these with [JsonConverter(typeof(MyConverterClass))] attributes on the BASE CLASSES ONLY (adding this attribute to a child class will cause a loop within the serializer it seems.)

  2. Added an enum property which overrides the base class and thereby denotes the derived class type, serving as a discriminator

  3. Created an appropriate converter class (in the same project as the DTOs) on the lines of the following

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]
    }

Converter example (the code is essentially the same between input and output DTOs)

    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);
        }
    }

Furthermore, a generic approach seems possible, although this code is not optimised or performance tested, I expect performance penalties from use of reflection and instantiation of objects using Activator.CreateInstance() to check the value of their discriminator.

Note that the below assumes that the discriminator property is an enum, and that the derived class has this property named exactly the same as the enumerated type:

Used as follows:

    [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);
        }

    }

Inspiration for the above / further reading: https://getyourbitstogether.com/polymorphic-serialization-in-system-text-json/ https://vpaulino.wordpress.com/2021/02/23/deserializing-polymorphic-types-with-system-text-json/ https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-6-0 https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonconverter-1?view=net-6.0 https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonconverterattribute?view=net-6.0

 

 

https://docs.abp.io/en/abp/4.4/Migration-Guides/Abp-4_0#unsupported-types

 

 

 

 

-------------->@@abp 4.4  PreConfigure<AbpJsonOptions>(options =>

https://github.com/abpframework/abp/issues/12604

Ultre00 commented on May 16, 2022
  • Version 5.2.1

Created a a new ABP project with the cli. In the HttpApiHost Module I added this:

public override void PreConfigureServices(ServiceConfigurationContext context)
{
    PreConfigure<AbpJsonOptions>(options =>
    {
        options.UseHybridSerializer = false;
        options.DefaultDateTimeFormat = "yyyy/MM/dd HH:mm:ss";
    });
}

Added a simple application service:

public class DemoDto
{
    public DateTime DateTest { get; set; }
}

public class DemoAppService : BookStoreAppService
{        
        
    public DemoDto GetDemo()
    {
        return new DemoDto()
        {
            DateTest = DateTime.UtcNow
        };
    } 
}

When testing the endpoint the result was:

{
  "dateTest": "2022-05-16T08:27:48.6620871Z"
}

How can I get the datetime to serialize as the provided AbpJsonOptions.DefaultDateTimeFormat ?

The actual issue I am facing in my original project is that the 'Z' is missing in the end. So when a datetime is serialized it shows as 2022-05-12T22:22:08.8273489 instead of 2022-05-12T22:22:08.8273489Z

 
  @maliming   Member maliming commented on May 16, 2022

The actual issue I am facing in my original project is that the 'Z' is missing in the end. So when a datetime is serialized it shows as 2022-05-12T22:22:08.8273489 instead of 2022-05-12T22:22:08.8273489Z

This is a typical XY problem, please describe your original problem in detail.

https://xyproblem.info/

相关文章

  • WebAssembly 使用
    1.安装Emscripten(用来编译到WebAssembly(wasm))gitclonehttps://github.com/emscripten-core/emsdk.gitcdemsdkgitpull#下面步骤用cmd操作emsdkinstalllatest//下载并安装最新的SDK工具(需要点时间)emsdkactivatelatest//使当前用户的“最新”SDK处于“......
  • java中的BO根DTO的区别以及使用场景
    java中的BO根DTO的区别以及使用场景  BO(BusinessObject)业务对象BO就是PO(PersistantObject)的组合简单的例子比如说PO是一条交易记录,BO是一个人全部的交易记录集合对象复杂点儿的例子PO1是交易记录,PO2是登录记录,PO3是商品浏览记录,PO4是添加购物车记录,PO5是搜索记录,BO是个......
  • DO、DTO、VO
    这三个不同名称的对象,在不同的公司可能由不同的作用,下面介绍的是通常这三个不同名称的对象的使用范围。DO(DataObject):(对接数据库)DO是指数据对象,它通常与数据库表或持久化层中的实体对象相对应。DO用于封装和表示与数据存储相关的数据,并与数据库进行交互。DO通常包含与数据库......
  • WebAssembly能不能取代JavaScript?15张卡通图给你答案!
    一切能用JavaScript实现的,终将用JavaScript实现。一切能编译为WebAssembly的,终将编译为WebAssembly。前端er们,WebAssembly用上了吗?在浏览器中快速运行非JavaScript语言,比如C、C++、Rust,是不是很香?今天,我们就来用15张小画图说WebAssembly。有必要先介绍一下小画的创作者。她叫LinCl......
  • DTO 与 Entity的区别
    entity:实体类,与数据库中的字段保持一致,用于表示某实体中所包含的所有属性。DTO:数据传输对象,用于数据传输,根据业务需求来决定包含哪些属性。根据实际业务需求将数据返回给前端,避免造成不必要的资源浪费和数据暴露,造成不必要的安全问题。......
  • api返回统一格式Dto
    usingCloudcubic.Common;namespaceCloudCubic.Model.DTO.Base{///<summary>///2023-04-23新增///</summary>///<typeparamname="T"></typeparam>publicclassApiResult<T>:ApiResultBase{......
  • 一文理解什么是DTO、VO、BO、PO、DO,并推荐一款IDEA转换插件
     1、什么是DTO、VO、BO、PO、DO、POJOPOJO的定义是无规则简单的对象,在日常的代码分层中pojo会被分为VO、BO、PO、DTO。通过各层POJO的使用,有助于提高代码的可读性和可维护性。概念看似简单,但是想区分好或者理解好也不容易,本文简单梳理一下。DTO(DataTransferObject)数据传......
  • VO,DTO,BO,POJO,PO的概念介绍
     po:1.po:popersistentobject持久对象,持久对象的意思指的是可以从内存中存储到关系型数据库中。2.因此一个po对应的数据库中的每一条记录。pojo:1.pojo:plainordinaryjavaobject无规则简单java对象,对应的是我们代码中的实体类。2.pojo持久化之后就是po了,可以看作一个中......
  • iOS 3DTouch
    概述iOS10系统登录中国,在系统中对3DTouch的使用需求更频繁,所以对iOS9中便引入的3DTouch功能做一些了解是很有必要的详细概述iOS10系统登录中国,在系统中对3DTouch的使用需求更频繁,所以对iOS9中便引入的3DTouch功能做一些了解是很有必要的在日常开发中,我们经......
  • 深入理解 DAO,DTO,DO,VO,AO,BO,POJO,PO,Entity,Model,View 各个模型对象的概念
    参考文档:https://blog.csdn.net/SR02020/article/details/105821816 深入理解DAO,DTO,DO,VO,AO,BO,POJO,PO,Entity,Model,View的概念DAO(DataAccessObject)数据访问对象DTO(DataTransferObject)数据传输对象DO(DomainObject)领域对象VO(ViewObject)视图模型AO(ApplicationObject)应用对象......