【问题标题】:Deserialising JSON to derived types in Asp.Net Web API在 Asp.Net Web API 中将 JSON 反序列化为派生类型
【发布时间】:2012-09-20 06:19:47
【问题描述】:

我正在调用我的 WebAPI 的一个方法,发送一个我想与模型匹配(或绑定)的 JSON。

在控制器中我有一个类似的方法:

public Result Post([ModelBinder(typeof(CustomModelBinder))]MyClass model);

作为参数给出的'MyClass'是一个抽象类。我希望根据传递的 json 类型,实例化正确的继承类。

为了实现它,我正在尝试实现一个自定义活页夹。问题是(我不知道它是否非常基本,但我找不到任何东西)我不知道如何检索请求中的原始 JSON(或者更好的是某种序列化)。

我明白了:

  • actionContext.Request.Content

但所有方法都公开为异步。我不知道这适合将生成模型传递给控制器​​方法...

【问题讨论】:

    标签: c# asp.net-mvc asp.net-web-api


    【解决方案1】:

    您不需要自定义模型绑定器。您也不需要处理请求管道。

    看看另一个 SO:How to implement custom JsonConverter in JSON.NET to deserialize a List of base class objects?

    我以此作为我自己解决同一问题的基础。

    从该 SO 中引用的 JsonCreationConverter<T> 开始(稍作修改以解决响应中类型序列化的问题):

    public abstract class JsonCreationConverter<T> : JsonConverter
    {
        /// <summary>
        /// this is very important, otherwise serialization breaks!
        /// </summary>
        public override bool CanWrite
        {
            get
            {
                return false;
            }
        }
        /// <summary> 
        /// Create an instance of objectType, based properties in the JSON object 
        /// </summary> 
        /// <param name="objectType">type of object expected</param> 
        /// <param name="jObject">contents of JSON object that will be 
        /// deserialized</param> 
        /// <returns></returns> 
        protected abstract T Create(Type objectType, JObject jObject);
    
        public override bool CanConvert(Type objectType)
        {
            return typeof(T).IsAssignableFrom(objectType);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType,
          object existingValue, JsonSerializer serializer)
        {
            if (reader.TokenType == JsonToken.Null)
                return null;
            // Load JObject from stream 
            JObject jObject = JObject.Load(reader);
    
            // Create target object based on JObject 
            T target = Create(objectType, jObject);
    
            // Populate the object properties 
            serializer.Populate(jObject.CreateReader(), target);
    
            return target;
        }
    
        public override void WriteJson(JsonWriter writer, object value, 
          JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    } 
    

    现在您可以使用 JsonConverterAttribute 注释您的类型,将 Json.Net 指向自定义转换器:

    [JsonConverter(typeof(MyCustomConverter))]
    public abstract class BaseClass{
      private class MyCustomConverter : JsonCreationConverter<BaseClass>
      {
         protected override BaseClass Create(Type objectType, 
           Newtonsoft.Json.Linq.JObject jObject)
         {
           //TODO: read the raw JSON object through jObject to identify the type
           //e.g. here I'm reading a 'typename' property:
    
           if("DerivedType".Equals(jObject.Value<string>("typename")))
           {
             return new DerivedClass();
           }
           return new DefaultClass();
    
           //now the base class' code will populate the returned object.
         }
      }
    }
    
    public class DerivedClass : BaseClass {
      public string DerivedProperty { get; set; }
    }
    
    public class DefaultClass : BaseClass {
      public string DefaultProperty { get; set; }
    }
    

    现在您可以使用基本类型作为参数:

    public Result Post(BaseClass arg) {
    
    }
    

    如果我们要发帖:

    { typename: 'DerivedType', DerivedProperty: 'hello' }
    

    那么arg 将是DerivedClass 的一个实例,但如果我们发布:

    { DefaultProperty: 'world' }
    

    然后你会得到一个DefaultClass 的实例。

    编辑 - 为什么我更喜欢这种方法而不是 TypeNameHandling.Auto/All

    我确实相信使用 JotaBe 支持的 TypeNameHandling.Auto/All 并不总是理想的解决方案。在这种情况下很可能是这样 - 但我个人不会这样做,除非:

    • 我的 API只有会被我或我的团队使用
    • 我不在乎拥有双 XML 兼容端点

    当使用 Json.Net TypeNameHandling.AutoAll 时,您的 Web 服务器将开始以 MyNamespace.MyType, MyAssemblyName 格式发送类型名称。

    我在 cmets 中说过,我认为这是一个安全问题。在我从 Microsoft 阅读的一些文档中提到了这一点。它似乎不再被提及,但我仍然觉得这是一个有效的担忧。我永远不想向外界公开命名空间限定的类型名称和程序集名称。它增加了我的攻击面。所以,是的,我的 API 类型不能有Object 属性/参数,但谁能说我的网站的其余部分完全没有漏洞?谁说未来的端点不会暴露利用类型名称的能力?为什么要抓住这个机会,因为它更容易?

    另外 - 如果您正在编写一个“适当的”API,即专门供第三方使用,而不仅仅是供您自己使用,并且您正在使用 Web API,那么您很可能希望利用 JSON/XML内容类型处理(至少)。看看您在尝试编写易于使用的文档方面取得了多大的成就,这些文档针对 XML 和 JSON 格式以不同的方式引用您的所有 API 类型。

    通过重写 JSON.Net 对类型名称的理解方式,您可以使两者保持一致,为您的调用者在 XML/JSON 之间进行选择完全基于口味,而不是因为类型名称更容易记住一个或另一个。

    【讨论】:

    • 已在您的帖子中添加了一条评论,说明为什么您的解决方案虽然正确,但不应真正“在野外”使用。
    • 为了表明我不只是一个混蛋——我已经 +1 了你的答案,因为它在一定程度上是有效的——并且很可能是最简单的方法问题。我只是不认为它在所有情况下都应该被视为灵丹妙药。
    • 我已经更新了我的答案,以反映您的解决方案何时是最佳选择。我认为这是一次非常有建设性的讨论。您也有我的 +1,因为您的解决方案是对 JSON 反序列化过程进行各种自定义的完美起点。而现在,这两个答案都得到了改善。
    • 只是为了添加注释。过去我使用了 $type 解决方案,但是对于我现在正在使用 AngularJS 进行的项目,它在 json 中用 $ 剥离任何东西时存在问题。此外,使用 TypeScript 类,我无法弄清楚如何确保 $type 包含在 JSON 序列化中。我确信这些问题有解决方案,但这种方法让我能够轻松克服这些问题。
    • 我对 $type 最大的抱怨是我不想在重构服务器端代码时破坏 API 兼容性,例如重命名类型或将其移动到另一个命名空间或程序集。例如,存储在客户端存储中并在以后使用的 DTO 将突然不再工作。如果改为使用枚举值或字符串,则可以完全控制 API 的向后兼容性。
    【解决方案2】:

    您不需要自己实现它。 JSON.NET 对它有原生支持。

    您必须为 JSON 格式化程序指定 desired TypeNameHandling option,如下所示(在 global.asax 应用程序启动事件中):

    JsonSerializerSettings serializerSettings = GlobalConfiguration.Configuration
       .Formatters.JsonFormatter.SerializerSettings;
    serializerSettings.TypeNameHandling = TypeNameHandling.Auto;
    

    如果您指定Auto,就像上面的示例一样,参数将被反序列化为对象的$type 属性中指定的类型。如果$type 属性缺失,它将被反序列化为参数的类型。因此,您只需在传递派生类型的参数时指定类型。 (这是最灵活的选项)。

    例如,如果您将此参数传递给 Web API 操作:

    var param = {
        $type: 'MyNamespace.MyType, MyAssemblyName', // .NET fully qualified name
        ... // object properties
    };
    

    参数将被反序列化为MyNamespace.MyType类的对象。

    这也适用于子属性,即你可以有一个像这样的对象,它指定一个内部属性是给定类型的

    var param = { 
       myTypedProperty: {
          $type: `...`
          ...
    };
    

    在这里你可以看到sample on JSON.NET documentation of TypeNameHandling.Auto

    This works at least since JSON.NET 4 release.

    注意

    你不需要用属性来装饰任何东西,或者做任何其他的定制。它无需更改您的 Web API 代码即可工作。

    重要提示

    The $type must be the first property of the JSON serialized object。如果不是,它将被忽略。

    与自定义 JsonConverter/JsonConverterAttribute 的比较

    我在比较原生解决方案to this answer

    实现JsonConverter/JsonConverterAttribute

    • 您需要实现自定义JsonConverter,以及自定义JsonConverterAttribute
    • 你需要使用属性来标记参数
    • 您需要事先了解参数预期的可能类型
    • 只要您的类型或属性发生变化,您就需要实现或更改JsonConverter 的实现
    • magic strings 的代码味道,表示预期的属性名称
    • 您没有实现可用于任何类型的通用内容
    • 你正在重新发明轮子

    在答案的作者中有一条关于安全的评论。除非你做错了什么(比如为你的参数接受一个过于通用的类型,比如Object),否则不会有获得错误类型实例的风险:JSON.NET 本机解决方案只实例化参数类型的对象,或者派生自它的类型(如果不是,你会得到null)。

    而这些是 JSON.NET 原生解决方案的优势:

    • 你不需要实现任何东西(你只需要在你的应用中配置一次TypeNameHandling
    • 您无需在操作参数中使用属性
    • 您不需要事先知道可能的参数类型:您只需要知道基本类型,并在参数中指定它(可以是抽象类型,使多态性更加明显)
    • 该解决方案适用于大多数情况(1),无需更改任何内容
    • 此解决方案经过广泛测试和优化
    • 你不需要魔术字符串
    • 实现是通用的,可以接受任何派生类型

    (1):如果你想接收不继承自相同基类型的参数值,这将不起作用,但我认为这样做没有意义

    所以我找不到任何缺点,并在 JSON.NET 解决方案上找到了许多优点。

    为什么要使用自定义 JsonConverter/JsonConverterAttribute

    这是一个很好的工作解决方案,允许自定义,可以修改或扩展以适应您的特定情况。

    如果你想做一些原生解决方案无法做到的事情,比如自定义类型名称,或者根据可用的属性名称推断参数的类型,那么请使用适合你自己情况的解决方案。另一个无法自定义,无法满足您的需求。

    【讨论】:

    • 是的,你是对的 - 但这是不适合我的情况(包括我的情况)的原因。使用 Json.Net 的内置类型名称处理功能存在 安全问题,因为它有效地允许恶意调用者绑定您的任何类型 - 或任何 .Net 类型。该文档对此进行了具体说明。我的解决方案提供了一个抽象,您可以精确控制可以绑定的类型。
    • JotaBe - 在没有首先考虑到这是一个适当缩减的答案的情况下,要非常小心地小跑“不通用”、“没有经过良好测试”和“可能性能较差”的短语,以免提供一堵代码墙,但很容易测试和扩展。其次,我基于此的解决方案是在非常繁忙的 Web API 环境中实现的更广泛的解决方案,它表现出色。是的 - $type 的东西在那里,并且有效。将安全问题视为“不是用例”是幼稚的。你也应该带着SerializationBinder 来回答我的观点。
    • Web API 文档中很早就提到了对安全性的担忧——而且,是的,谷歌似乎已经放弃了这一点。公平的做法;但我不是骗子,我确实读过这些担忧。即使没有其他人支持 - 我个人不想在我的 JSON 中编码 MyNamespace.MyType, MyAssembly 类型名称 - 我不想将我的真实类型名称任何暴露给外界;当一个简单的TypeName 就足够时,通过 JSON 使用我的 API 的第三方也不想使用 .Net 类型名称。不过,每个人都有自己的特点。
    • 我看到的唯一安全问题是使用通用参数类型,如对象或动态,这是唯一允许实例化任意对象的情况。我只是想知道我是否还缺少其他东西。嘿!,我不是想看不起你的解决方案,或者认为你在撒谎,一点也不:我想表达一个公平的意见。我真的认为 JSON.NET 解决方案更通用,开箱即用(只需要一个参数基类),经过广泛的现场测试(在 Nuget 中有近 6M 的下载量),这比任何经过深思熟虑的单元测试套件都可以进行更多测试实施
    • 好吧,那么您应该公平地说,TypeNameHandling 工作正常,这是一个很好的解决方案除非存在无法处理的问题。在这种情况下,您提供了一个经过测试的解决方案,该解决方案展示了如何进行反序列化的自定义实现,这对于需要特殊自定义的许多其他人很有用(例如,在您的情况下,您不想使用 .NET 完全限定名称) .我同意这一点!!对于这些情况,这是一个很好的解决方案(而且一点也不明显)。但是原生解决方案对大多数人来说仍然很好。
    【解决方案3】:

    您可以正常调用异步方法,您的执行将被暂停,直到方法返回,您可以以标准方式返回模型。只需像这样拨打电话:

    string jsonContent = await actionContext.Request.Content.ReadAsStringAsync();
    

    它将为您提供原始 JSON。

    【讨论】:

    【解决方案4】:

    如果您想使用 TypeNameHandling.Auto 但担心安全性或不喜欢需要该级别幕后知识的 api 使用者,您可以处理 $type 反序列化您自己。

    public class InheritanceSerializationBinder : DefaultSerializationBinder
    {
        public override Type BindToType(string assemblyName, string typeName)
        {
            switch (typeName)
            {
                case "parent[]": return typeof(Class1[]);
                case "parent": return typeof(Class1);
                case "child[]": return typeof(Class2[]);
                case "child": return typeof(Class2);
                default: return base.BindToType(assemblyName, typeName);
            }
        }
    }
    

    然后将其连接到 global.asax.Application__Start

    var config = GlobalConfiguration.Configuration;
            config.Formatters.JsonFormatter.SerializerSettings = new JsonSerializerSettings { Binder = new InheritanceSerializationBinder() };
    

    最后,我在包含不同类型对象的属性上使用了包装类和 [JsonProperty(TypeNameHandling = TypeNameHandling.Auto)],因为我无法通过配置实际类来使其工作。

    这种方法允许消费者在他们的请求中包含所需的信息,同时允许允许值的文档独立于平台、易于更改且易于理解。所有这些都无需编写您自己的转换器。

    归功于:https://mallibone.com/post/serialize-object-inheritance-with-json.net 向我展示了该字段属性的自定义反序列化器。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-06-05
      • 1970-01-01
      • 2017-03-22
      • 2020-12-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多