ASP.NET Core API的JSON响应中缺少派生类型的属性 [英] Derived type's properties missing in JSON response from ASP.NET Core API

查看:138
本文介绍了ASP.NET Core API的JSON响应中缺少派生类型的属性的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的ASP.NET Core 3.1 API控制器的JSON响应缺少属性。当属性使用派生类型时,就会发生这种情况。在派生类型中定义但在基类/接口中没有定义的任何属性都不会序列化为JSON。响应中似乎缺少对多态的支持,好像序列化基于属性的定义类型而不是其运行时类型。我如何更改此行为以确保JSON响应中包含所有公共属性?

The JSON response from my ASP.NET Core 3.1 API controller is missing properties. This happens when a property uses a derived type; any properties defined in the derived type but not in the base/interface will not be serialized to JSON. It seems there is some lack of support for polymorphism in the response, as if serialization is based on a property's defined type instead of its runtime type. How can I change this behavior to ensure that all public properties are included in the JSON response?

示例

我的.NET Core Web API控制器返回此对象,该对象具有接口类型的属性。

My .NET Core Web API Controller returns this object that has a property with an interface type.

    // controller returns this object
    public class Result
    {
        public IResultProperty ResultProperty { get; set; }   // property uses an interface type
    }

    public interface IResultProperty
    { }

此处是派生类型,用于定义名为 Value 的新公共属性。

Here is a derived type that defines a new public property named Value.

    public class StringResultProperty : IResultProperty
    {
        public string Value { get; set; }
    }

如果我从控制器返回派生类型,如下所示:

If I return the derived type from my controller like this:

    return new MainResult {
        ResultProperty = new StringResultProperty { Value = "Hi there!" }
    };

然后实际响应包括一个空对象( Value 属性丢失):

then the actual response includes an empty object (the Value property is missing):

我希望响应为:

    {
        "ResultProperty": { "Value": "Hi there!" }
    }


推荐答案

我最终创建了一个自定义JsonConverter(System.Text.Json.Serialization命名空间),它强制JsonSerializer序列化为对象的 runtime 类型。请参阅下面的解决方案部分。它很长,但是效果很好,不需要我在API的设计中牺牲面向对象的原理。

I ended up creating a custom JsonConverter (System.Text.Json.Serialization namespace) which forces JsonSerializer to serialize to the object's runtime type. See the Solution section below. It's lengthy but it works well and does not require me to sacrifice object oriented principles in my API's design.

某些背景:微软拥有一个系统.Text.Json序列化指南,标题为对派生类的属性进行序列化,并提供与我的问题相关的良好信息。特别是它解释了为什么未对派生类型的属性进行序列化的原因:

Some background: Microsoft has a System.Text.Json serialization guide with a section titled Serialize properties of derived classes with good information relevant to my question. In particular it explains why properties of derived types are not serialized:


此行为旨在帮助防止意外泄露数据

This behavior is intended to help prevent accidental exposure of data in a derived runtime-created type.

如果您不关心此问题,则可以在对 JsonSerializer.Serialize 通过显式指定派生类型或通过指定 object ,例如:

If that is not a concern for you then the behavior can be overridden in the call to JsonSerializer.Serialize by either explicitly specifying the derived type or by specifying object, for example:

// by specifying the derived type
jsonString = JsonSerializer.Serialize(objToSerialize, objToSerialize.GetType(), serializeOptions);

// or specifying 'object' works too
jsonString = JsonSerializer.Serialize<object>(objToSerialize, serializeOptions);

要使用ASP.NET Core完成此操作,您需要加入序列化过程。我通过调用JsonSerializer的自定义JsonConverter做到了这一点。序列化上面显示的方法之一。我还实现了对反序列化的支持,尽管在原始问题中没有明确要求,但无论如何几乎都需要。 (奇怪的是,仅支持序列化而不支持反序列化确实很棘手。)

To accomplish this with ASP.NET Core you need to hook into the serialization process. I did this with a custom JsonConverter that calls JsonSerializer.Serialize one of the ways shown above. I also implemented support for deserialization which, while not explicitly asked for in the original question, is almost always needed anyway. (Oddly, supporting only serialization and not deserialization proved to be tricky anyway.)

解决方案

我创建了一个基类 DerivedTypeJsonConverter ,其中包含所有序列化&反序列化逻辑。对于每个基本类型,您将为其创建一个相应的转换器类,该转换器类从 DerivedTypeJsonConverter 派生。

I created a base class, DerivedTypeJsonConverter, which contains all of the serialization & deserialization logic. For each of your base types, you would create a corresponding converter class for it that derives from DerivedTypeJsonConverter. This is explained in the numbered directions below.

此解决方案遵循类型名称处理 约定引入了对JSON多态性的支持。它通过在派生类型的JSON中包含一个附加的 $ type 属性(例如: $ type: StringResultProperty )来告诉转换器对象的真实类型是什么。 (一个区别:在Json.NET中,$ type的值是完全限定的类型+程序集名称,而我的$ type是一个自定义字符串,有助于将来防止名称空间/程序集/类名更改。)API调用程序应包括JSON请求中的$ type属性用于派生类型。序列化逻辑通过确保对对象的所有公共属性进行序列化来解决我的原始问题,并且为了一致性,还对$ type属性进行了序列化。

This solution follows the "type name handling" convention from Json.NET which introduces support for polymorphism to JSON. It works by including an additional $type property in the derived type's JSON (ex: "$type":"StringResultProperty") that tells the converter what the object's true type is. (One difference: in Json.NET, $type's value is a fully qualified type + assembly name, whereas my $type is a custom string which helps future-proof against namespace/assembly/class name changes.) API callers are expected to include $type properties in their JSON requests for derived types. The serialization logic solves my original problem by ensuring that all of the object's public properties are serialized, and for consistency the $type property is also serialized.

方向:

1)将下面的DerivedTypeJsonConverter类复制到您的项目中。

1) Copy the DerivedTypeJsonConverter class below into your project.

using System;
using System.Collections.Generic;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

public abstract class DerivedTypeJsonConverter<TBase> : JsonConverter<TBase>
{
    protected abstract string TypeToName(Type type);

    protected abstract Type NameToType(string typeName);


    private const string TypePropertyName = "$type";


    public override bool CanConvert(Type objectType)
    {
        return typeof(TBase) == objectType;
    }


    public override TBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        // get the $type value by parsing the JSON string into a JsonDocument
        JsonDocument jsonDocument = JsonDocument.ParseValue(ref reader);
        jsonDocument.RootElement.TryGetProperty(TypePropertyName, out JsonElement typeNameElement);
        string typeName = (typeNameElement.ValueKind == JsonValueKind.String) ? typeNameElement.GetString() : null;
        if (string.IsNullOrWhiteSpace(typeName)) throw new InvalidOperationException($"Missing or invalid value for {TypePropertyName} (base type {typeof(TBase).FullName}).");

        // get the JSON text that was read by the JsonDocument
        string json;
        using (var stream = new MemoryStream())
        using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Encoder = options.Encoder })) {
            jsonDocument.WriteTo(writer);
            writer.Flush();
            json = Encoding.UTF8.GetString(stream.ToArray());
        }

        // deserialize the JSON to the type specified by $type
        try {
            return (TBase)JsonSerializer.Deserialize(json, NameToType(typeName), options);
        }
        catch (Exception ex) {
            throw new InvalidOperationException("Invalid JSON in request.", ex);
        }
    }


    public override void Write(Utf8JsonWriter writer, TBase value, JsonSerializerOptions options)
    {
        // create an ExpandoObject from the value to serialize so we can dynamically add a $type property to it
        ExpandoObject expando = ToExpandoObject(value);
        expando.TryAdd(TypePropertyName, TypeToName(value.GetType()));

        // serialize the expando
        JsonSerializer.Serialize(writer, expando, options);
    }


    private static ExpandoObject ToExpandoObject(object obj)
    {
        var expando = new ExpandoObject();
        if (obj != null) {
            // copy all public properties
            foreach (PropertyInfo property in obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead)) {
                expando.TryAdd(property.Name, property.GetValue(obj));
            }
        }

        return expando;
    }
}

2)每个您的基本类型中,创建一个从DerivedTypeJsonConverter派生的类。实现将mappig $ type字符串转换为实际类型的2个抽象方法。这是您可以遵循的IResultProperty接口的示例。

2) For each of your base types, create a class that derives from DerivedTypeJsonConverter. Implement the 2 abstract methods which are for mappig $type strings to actual types. Here is an example for my IResultProperty interface that you can follow.

public class ResultPropertyJsonConverter : DerivedTypeJsonConverter<IResultProperty>
{
    protected override Type NameToType(string typeName)
    {
        return typeName switch
        {
            // map string values to types
            nameof(StringResultProperty) => typeof(StringResultProperty)

            // TODO: Create a case for each derived type
        };
    }

    protected override string TypeToName(Type type)
    {
        // map types to string values
        if (type == typeof(StringResultProperty)) return nameof(StringResultProperty);

        // TODO: Create a condition for each derived type
    }
}

3)在Startup.cs中注册转换器。

3) Register the converters in Startup.cs.

services.AddControllers()
    .AddJsonOptions(options => {
        options.JsonSerializerOptions.Converters.Add(new ResultPropertyJsonConverter());

        // TODO: Add each converter
    });

4)在对API的请求中,需要派生类型的对象包含$ type属性。 JSON示例: { Value: Hi!, $ type: StringResultProperty}

4) In requests to the API, objects of derived types will need to include a $type property. Example JSON: { "Value":"Hi!", "$type":"StringResultProperty" }

此处要点全部

这篇关于ASP.NET Core API的JSON响应中缺少派生类型的属性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆