将Json反序列化为Asp.Net Web API中的派生类型

Jac*_*cob 59 c# asp.net-mvc asp.net-web-api

我正在调用我的WebAPI的方法,发送一个我希望与模型匹配(或绑定)的json.

在控制器中我有一个方法,如:

public Result Post([ModelBinder(typeof(CustomModelBinder))]MyClass model);
Run Code Online (Sandbox Code Playgroud)

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

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

我知道了:

  • actionContext.Request.Content

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

非常感谢!

And*_*tan 91

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

看看这个其他的SO:如何在JSON.NET中实现自定义JsonConverter来反序列化基类对象的列表?.

我用它作为我自己解决同一问题的基础.

JsonCreationConverter<T>SO中的引用开始(略微修改以修复响应中类型序列化的问题):

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();
    }
} 
Run Code Online (Sandbox Code Playgroud)

现在,您可以使用将JsonConverterAttributeJson.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; }
}
Run Code Online (Sandbox Code Playgroud)

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

public Result Post(BaseClass arg) {

}
Run Code Online (Sandbox Code Playgroud)

如果我们发布:

{ typename: 'DerivedType', DerivedProperty: 'hello' }
Run Code Online (Sandbox Code Playgroud)

然后arg将是一个实例DerivedClass,但如果我们发布:

{ DefaultProperty: 'world' }
Run Code Online (Sandbox Code Playgroud)

然后你会得到一个实例DefaultClass.

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

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

  • 我的API 只会由我或我的团队使用
  • 我不关心拥有双XML兼容端点

当Json.Net TypeNameHandling.AutoAll使用,Web服务器将开始在格式发出类型名称MyNamespace.MyType, MyAssemblyName.

我在评论中说过,我认为这是一个安全问题.在我从微软阅读的一些文档中提到了这一点.它似乎没有被提及,但我仍然觉得这是一个有效的问题.我不以往任何时候都希望名称空间限定的类型名称和程序集名称暴露给外界.它增加了我的攻击面.所以,是的,我不能拥有Object我的API类型的属性/参数,但谁能说我的网站的其余部分完全没有空洞?谁说未来的端点没有公开利用类型名称的能力?为什么选择这个机会只是因为它更容易?

另外 - 如果您正在编写一个"正确的"API,即专门供第三方使用而不仅仅是为了您自己,并且您正在使用Web API,那么您很可能希望利用JSON/XML内容类型处理(至少).了解您尝试编写易于使用的文档的程度,它指的是XML和JSON格式的所有API类型.

通过重写JSON.Net如何理解类型名称,您可以将两者放在一起,使您的调用者之间的XML/JSON之间的选择完全基于品味,而不是因为类型名称在一个或另一个中更容易记住.

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

Jot*_*aBe 49

您不需要自己实现它.JSON.NET对它有本机支持.

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

JsonSerializerSettings serializerSettings = GlobalConfiguration.Configuration
   .Formatters.JsonFormatter.SerializerSettings;
serializerSettings.TypeNameHandling = TypeNameHandling.Auto;
Run Code Online (Sandbox Code Playgroud)

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

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

var param = {
    $type: 'MyNamespace.MyType, MyAssemblyName', // .NET fully qualified name
    ... // object properties
};
Run Code Online (Sandbox Code Playgroud)

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

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

var param = { 
   myTypedProperty: {
      $type: `...`
      ...
};
Run Code Online (Sandbox Code Playgroud)

在这里,您可以看到TypeNameHandling.Auto的JSON.NET文档中示例.

这至少在JSON.NET 4发布之后起作用.

注意

您不需要使用attirbutes来装饰任何东西,也不需要进行任何其他自定义.它将在您的Web API代码中不做任何更改的情况下工作.

重要的提示

$ type必须是JSON序列化对象的第一个属性.如果没有,它将被忽略.

与CUSTOM JsonConverter/JsonConverterAttribute的比较

我正在将本机解决方案与此答案进行比较.

实现JsonConverter/ JsonConverterAttribute:

  • 您需要实现自定义JsonConverter和自定义JsonConverterAttribute
  • 您需要使用属性来标记参数
  • 您需要事先知道参数所需的可能类型
  • 您需要JsonConverter在类型或属性发生变化时实施或更改您的实现
  • 有一个魔术字符串的代码气味,以指示预期的属性名称
  • 你没有实现可以用于任何类型的通用的东西
  • 你正在重新发明轮子

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

这些是JSON.NET本机解决方案的优点:

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

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

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

为什么使用CUSTOM JsonConverter/JsonConverterAttribute

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

如果要执行本机解决方案无法执行的操作(如自定义类型名称)或根据可用属性名称推断参数类型,请使用适合您自己案例的此解决方案.另一个无法定制,无法满足您的需求.

  • 好吧,那么你应该公平,并说'TypeNameHandling`工作正常,这是一个很好的解决方案**除非有人担心它无法处理**.在这种情况下,您提供了一个经过测试的解决方案,该解决方案展示了如何进行反序列化的自定义实现,这对于需要特殊自定义的许多其他人非常有用(例如,在您的情况下,您不希望使用.NET完全限定名称) .我同意这一点!对于那些案例来说这是一个很好的解决方案(并不是很明显).但原生解决方案对大多数人来说仍然有用. (3认同)