C# 在Asp.NETWebAPI中将JSON反序列化为派生类型

C# 在Asp.NETWebAPI中将JSON反序列化为派生类型,c#,asp.net-mvc,asp.net-web-api,C#,Asp.net Mvc,Asp.net Web Api,我正在调用WebAPI的一个方法,发送一个我希望与模型匹配或绑定的JSON 在控制器中,我有如下方法: public Result Post([ModelBinder(typeof(CustomModelBinder))]MyClass model); 作为参数提供的“MyClass”是一个抽象类。我希望根据传递的json类型,实例化正确的继承类 为了实现它,我正在尝试实现一个自定义绑定器。问题是,我不知道它是否非常基本,但我找不到任何我不知道如何检索原始JSON或更好的,请求中的某种序列化

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

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

public Result Post([ModelBinder(typeof(CustomModelBinder))]MyClass model);
作为参数提供的“MyClass”是一个抽象类。我希望根据传递的json类型,实例化正确的继承类

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

我明白了:

actionContext.Request.Content
但所有方法都以异步方式公开。我不知道将生成模型传递给控制器方法适合谁…

您可以正常调用异步方法,您的执行将被暂停,直到方法返回,并且您可以以标准方式返回模型。就这样打个电话吧:

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

它将为您提供原始JSON。

您不需要自定义模型绑定器。你也不需要在请求管道上乱搞

看看另一个,所以:

我用这个作为我自己解决同一问题的基础

从中引用的JsonCreationConverter开始,对其进行了轻微修改,以修复响应中类型序列化的问题:

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();
    }
} 
现在,可以将基类型用作参数:

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.Auto或All时,web服务器将开始以MyNamespace.MyType、MyAssemblyName的格式发送类型名称

我在评论中说过,我认为这是一个安全问题。在我从微软读到的一些文档中提到了这一点。它似乎不再被提及,但我仍然觉得这是一个合理的担忧。我不想向外界公开命名空间限定的类型名和程序集名。这增加了我的攻击面。所以,是的,我不能将对象属性/参数设置为我的API类型,但是谁能说我的站点的其余部分完全没有漏洞呢?谁说未来的端点不会暴露利用类型名的能力?为什么仅仅因为比较容易就要冒这个险呢

另外,如果您正在编写一个“适当的”API,即专门供第三方使用而不仅仅是供您自己使用,并且您正在使用Web API,那么您最有可能希望将JSON/XML内容类型处理作为最低限度。看看您在尝试编写易于使用的文档方面取得了多大进展,这些文档以不同的方式引用XML和JSON格式的所有API类型


通过覆盖JSON.Net理解类型名的方式,您可以将两者结合起来,使调用方在XML/JSON之间的选择完全基于品味,而不是因为类型名在其中一个中更容易记住。

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

您必须为JSON格式化程序指定格式,如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: `...`
      ...
};
在这里你可以看到一张照片

你不需要用阁楼装饰任何东西,也不需要做任何其他定制。它将在不更改Web API代码的情况下工作

重要提示

。如果没有,它将被忽略

与自定义JsonConverter/JsonConverterAttribute的比较

我正在比较本机解决方案

要实现JsonConverter/JsonConverterAttribute,请执行以下操作:

您需要实现一个自定义JsonConverter和一个自定义JsonConverterAttribute 您需要使用属性来标记参数 您需要事先知道参数的可能类型 每当类型或属性发生更改时,您需要实现或更改JsonConverter的实现 有一种气味的代码 删除所需的属性名称 您没有实现可用于任何类型的泛型 你在重新发明轮子 在答案的作者中有一条关于安全性的评论。除非您做了一些错误的事情,比如为您的参数(like Object)接受一个过于泛型的类型,否则不会有获取错误类型实例的风险:JSON.NET本机解决方案仅实例化参数类型的对象,或者如果不是,则获取null

以下是JSON.NET本机解决方案的优点:

您不需要实现任何东西,只需在应用程序中配置一次TypeNameHandling 您不需要在动作参数中使用属性 您不需要事先知道可能的参数类型:您只需要知道基类型,并在参数中指定它,它可能是一个抽象类型,以使多态性更加明显 该解决方案适用于大多数情况1,无需更改任何内容 此解决方案经过广泛测试和优化 你不需要魔法弦 该实现是泛型的,并且将接受任何派生类型 1:如果希望接收不从同一基类型继承的参数值,这将不起作用,但我认为这样做没有意义

所以我没有发现JSON.NET解决方案的任何缺点,也没有发现JSON.NET解决方案的许多优点

为什么使用自定义JsonConverter/JsonConverterAttribute

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


如果要执行本机解决方案无法执行的操作,如自定义类型名称,或根据可用的属性名称推断参数的类型,请使用适合您自己情况的解决方案。另一个不能自定义,也不能满足您的需要。

如果您想使用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\uuuu Start中

var config = GlobalConfiguration.Configuration;
        config.Formatters.JsonFormatter.SerializerSettings = new JsonSerializerSettings { Binder = new InheritanceSerializationBinder() };
最后,我在包含不同类型对象的Property上使用了包装类和[JsonPropertyTypeNameHandling=TypeNameHandling.Auto],因为我无法通过配置实际的类使其工作

这种方法允许使用者在其请求中包含所需的信息,同时允许允许值的文档独立于平台、易于更改和理解。不必写自己的对话

归功于:
用于向我显示该字段属性的自定义反序列化程序。

除非您对死锁的可能性感到满意,否则不要使用.Result。是的,您是对的,但这不适合我的情况,包括我的情况,这是有原因的。使用Json.Net的内置类型名处理功能存在严重的安全问题,因为它有效地允许恶意调用方绑定您的任何类型或任何.Net类型。文档对此进行了详细说明。我的解决方案提供了一种抽象,您可以精确控制可绑定的类型。JotaBe-非常小心地抛出“非泛型”短语,“测试得不太好”和“性能可能不太好”,没有首先考虑到这是一个适当的简化答案,这样就不会提供一堵代码墙,但很容易测试和扩展。其次,我所基于的解决方案是在非常繁忙的Web API环境中实现的一个更广泛的解决方案,它的性能非常出色。是的,美元类型的东西就在那里,而且很有效。将安全问题视为“不是用例”是天真的。你也应该带着SerializationBinder回来回答我的问题。Web API文档很早就提到了对安全性的担忧——是的,这似乎已经从Google上消失了。公平对待;但我不是一个骗子,我确实读过那些担忧。即使没有其他人的支持——我个人不想在JSON中编码MyNamespace.MyType、MyAssembly类型名称——我也不想向外界公开任何真实的类型名称;通过JSON使用我的API的第三方也不想在简单的类型名就足够的情况下使用.Net类型名。我所看到的唯一的安全问题是使用通用参数类型,如object或dynamic,这是唯一允许实例任意对象的情况。我只是想知道我是否还遗漏了什么。嘿我不是想看不起你的解决方案,也不是说你在撒谎,一点也不是:我是想表达一个公平的意见。我真的认为JSON.NET解决方案更通用,开箱即用,只需要一个参数基类,在Nuget中通过近600万次下载进行了广泛的现场测试,这比任何可以实现的深思熟虑的单元测试套件都要多。那么,您应该公平地说
搪瓷处理效果很好,这是一个很好的解决方案,除非有它无法处理的问题。在这种情况下,您提供了一个经过测试的解决方案,该解决方案展示了如何进行反序列化的自定义实现,该实现对于许多其他需要特殊自定义的人非常有用。例如,在您的情况下,您不希望使用.NET完全限定名。我同意!!对于这些情况,这是一个很好的解决方案,一点也不明显。但是,本机解决方案对大多数人来说仍然是好的。我在你的帖子中添加了一条评论,说明为什么你的解决方案虽然正确,但实际上不应该“在野外”使用。为了表明我不仅仅是一个a*洞,我有+1的答案,因为它在某种程度上是有效的,并且可能是解决这个问题的最简单的方法。我只是不认为它在任何情况下都是万能的。我已经更新了我的答案,以反映什么时候你的解决方案是最好的选择。我认为这是一次非常有建设性的讨论。您还有my+1,因为您的解决方案是对JSON反序列化过程进行各种定制的完美起点。现在,两个答案都有了改进。只需补充一点。在过去,我使用$type解决方案,但是对于我现在正在使用AngularJS的项目来说,在json中使用$type剥离任何内容都有问题。此外,使用TypeScript类,我无法确定如何确保JSON序列化中包含$type。我确信有解决这些问题的方法,但这种方法使我能够轻松克服这些问题。我对$type最大的不满是,我不想在重构服务器端代码时破坏API兼容性,例如重命名类型或将其移动到另一个命名空间或程序集。例如,存储在客户机存储中并在以后使用的DTO将突然不再工作。如果使用枚举值或字符串,则完全可以控制API中的向后兼容性。
var config = GlobalConfiguration.Configuration;
        config.Formatters.JsonFormatter.SerializerSettings = new JsonSerializerSettings { Binder = new InheritanceSerializationBinder() };