abp 4.0 dto jobject

发布时间 2023-07-17 16:14:32作者: dreamw

@@abp 4.0 dto jobject

 

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

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?

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
  • 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  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
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://stackoverflow.com/questions/70032776/abp-blazor-webassembly-polymorphic-dto-deserialization-using-system-text-json