web-dev-qa-db-ja.com

Asp.Net Web APIでJsonを派生型にデシリアライズする

モデルと一致(またはバインド)したいJSONを送信するWebAPIのメソッドを呼び出しています。

コントローラーには、次のようなメソッドがあります。

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

'MyClass'、これはパラメータとして抽象クラスです。渡されたJSONのタイプに応じて、正しい継承クラスがインスタンス化されるようにしたいと思います。

それを達成するために、カスタムバインダーを実装しようとしています。問題は(それが非常に基本的なものかどうかはわかりませんが、何も見つかりません)リクエストに含まれる生のJson(またはもっと良いのは、何らかのシリアル化)を取得する方法がわかりません。

そうですか:

  • actionContext.Request.Content

ただし、すべてのメソッドは非同期として公開されます。生成モデルをコントローラーメソッドに渡すのにこれが誰に当てはまるかわかりません...

どうもありがとう!

59
IoChaos

カスタムモデルバインダーは必要ありません。また、リクエストパイプラインをいじる必要もありません。

この他の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' }

argDerivedClassのインスタンスになりますが、投稿した場合:

{ DefaultProperty: 'world' }

次に、DefaultClassのインスタンスを取得します。

編集-TypeNameHandling.Auto/Allよりもこの方法を好む理由

私は、JotaBeが支持するTypeNameHandling.Auto/Allを使用することが常に理想的なソリューションであるとは考えていません。この場合はそうかもしれません-しかし、個人的に私はそれをしない:

  • 私のAPIはのみ私または私のチームによって使用されます
  • デュアルXML互換のエンドポイントを使用する必要はありません

Json.Net TypeNameHandling.AutoまたはAllが使用されると、WebサーバーはMyNamespace.MyType, MyAssemblyName形式で型名の送信を開始します。

これはセキュリティ上の懸念だと思うとコメントで述べました。これについては、Microsoftから読んだいくつかのドキュメントで言及されています。それはもう言及されていないようですが、それは妥当な懸念だとまだ感じています。名前空間で修飾された型名とアセンブリ名を外部に公開したくないever 。攻撃対象が増えています。それで、はい、私のAPIタイプのObjectプロパティ/パラメーターを持つことはできませんが、私のサイトの残りの部分は完全に穴がないと言えますか?将来のエンドポイントが型名を悪用する機能を公開しないと言うのは誰ですか?簡単だからといって、なぜそのチャンスをつかむのでしょうか?

また、「適切な」APIを作成している場合、つまり、自分だけでなくサードパーティが使用するためにWeb APIを使用している場合は、JSON/XMLコンテンツタイプを活用することをお勧めします。処理(少なくとも)。使いやすいドキュメントをどの程度作成しようとしているのかを確認してください。これは、XML形式とJSON形式ですべてのAPIタイプを別々に参照します。

JSON.Netが型名をどのように理解するかをオーバーライドすることで、2つを並べることができ、どちらか一方の型名が覚えやすいためではなく、純粋に好みに基づいて呼び出し元のXML/JSONを選択できます。

89
Andras Zoltan

自分で実装する必要はありません。 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リリース以降で機能します

[〜#〜] note [〜#〜]

属性で何かを装飾したり、他のカスタマイズを行う必要はありません。 Web APIコードを変更しなくても機能します。

重要な注意事項

$ typeはJSONシリアル化オブジェクトの最初のプロパティである必要があります 。そうでない場合、無視されます。

カスタムJsonConverter/JsonConverterAttributeとの比較

私はネイティブソリューションを比較しています この答えと

JsonConverter/JsonConverterAttributeを実装するには:

  • カスタムJsonConverterとカスタムJsonConverterAttributeを実装する必要があります
  • 属性を使用してパラメータをマークする必要があります
  • パラメータに予想されるタイプを事前に知る必要があります
  • タイプまたはプロパティが変更されるたびに、JsonConverterの実装を実装または変更する必要があります
  • 予想されるプロパティ名を示す マジックストリング のコード臭があります
  • どのタイプでも使用できる一般的なものを実装していない
  • あなたは車輪を再発明しています

回答の著者には、セキュリティに関するコメントがあります。何か間違ったことをしない限り(Objectのようにパラメーターに対して汎用的な型を受け入れるなど)、間違った型のインスタンスを取得するリスクはありません。JSON.NETネイティブソリューションは、パラメーターの型のオブジェクトのみをインスタンス化します、またはそれから派生したタイプ(そうでない場合は、nullを取得します)。

そして、これらはJSON.NETネイティブソリューションの利点です。

  • 何も実装する必要はありません(アプリでTypeNameHandlingを一度設定するだけです)
  • アクションパラメータで属性を使用する必要はありません
  • 可能なパラメーターの型を事前に知る必要はありません。単に基本型を知り、パラメーターで指定するだけです(多相性をより明確にするために、抽象型にすることもできます)
  • ソリューションはほとんどの場合に機能します(1)何も変更せずに
  • このソリューションは広くテストされ、最適化されています
  • マジックストリングは必要ありません
  • 実装は汎用的であり、派生型を受け入れます

(1):同じ基本型から継承しないパラメーター値を受け取りたい場合、これは機能しませんが、そうすることには意味がありません

だから私は短所を見つけることができず、JSON.NETソリューションで多くの利点を見つけます。

カスタムJsonConverter/JsonConverterAttributeを使用する理由

これは、カスタマイズを可能にする優れた実用的なソリューションであり、特定のケースに合わせて変更または拡張することができます。

型名のカスタマイズ、使用可能なプロパティ名に基づいたパラメーターの型の推測など、ネイティブソリューションではできないことをしたい場合は、独自のケースに合わせてこのソリューションを使用してください。もう1つはカスタマイズできず、ニーズに合わせて機能しません。

49
JotaBe

通常は非同期メソッドを呼び出すことができ、メソッドが戻るまで実行は一時停止され、標準的な方法でモデルを返すことができます。次のような電話をかけるだけです。

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

生のJSONが得られます。

4
tpeczek

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 そのフィールドプロパティのカスタムデシリアライザーを見せてくれた。

2
user8606451