63

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

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

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

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

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

我懂了:

  • actionContext.Request.Content

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

4

4 回答 4

94

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

看看另一个 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();
    }
} 

现在您可以使用 注释您的类型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

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

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

当使用 Json.NetTypeNameHandling.AutoAll时,您的 Web 服务器将开始以MyNamespace.MyType, MyAssemblyName.

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

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

通过覆盖 JSON.Net 对类型名称的理解方式,您可以使两者保持一致,为您的调用者在 XML/JSON 之间进行选择纯粹基于品味,而不是因为类型名称更容易记住其中一个。

于 2012-09-28T14:03:04.037 回答
49

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

您必须为 JSON 格式化程序指定所需的 TypeNameHandling 选项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: `...`
      ...
};

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

这至少从 JSON.NET 4 版本开始有效

笔记

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

重要的提示

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

与自定义 JsonConverter/JsonConverterAttribute 的比较

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

要实现JsonConverter/ JsonConverterAttribute

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

在答案的作者中有关于安全性的评论。除非你做错了什么(比如为你的参数接受一个过于泛型的类型,Object比如它(如果没有,你会得到null)。

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

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

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

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

为什么使用自定义 JsonConverter/JsonConverterAttribute

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

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

于 2014-06-02T16:23:06.177 回答
4

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

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

它会给你原始的 JSON。

于 2012-09-28T12:39:45.947 回答
3

如果您想使用 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 向我展示了该字段属性的自定义反序列化程序。

于 2018-03-08T14:00:13.143 回答