Deserialisieren von Json zu abgeleiteten Typen in Asp.Net Web API

Ich rufe eine Methode meines WebAPI auf, die einen json sendet, den ich mit einem Modell zusammenbringen (oder binden) möchte.

Im Controller habe ich eine Methode wie:

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

‘MyClass’, das als Parameter angegeben wird, ist eine abstrakte class. Ich möchte das bei, abhängig von der Art von json übergeben, die korrekte geerbte class instanziiert wird.

Um dies zu erreichen, versuche ich einen benutzerdefinierten Ordner zu implementieren. Das Problem ist, dass (ich weiß nicht, ob es sehr einfach ist, aber ich kann nichts finden) Ich weiß nicht, wie man die rohe Json (oder besser, irgendeine Art von Serialisierung), die in der Anfrage kommt, abrufen kann.

Aha:

  • actionContext.Request.Content

Alle Methoden werden jedoch als Async verfügbar gemacht. Ich weiß nicht, wer das passt, um das generierte Modell an die Controller-Methode zu übergeben …

Danke vielmals!

Solutions Collecting From Web of "Deserialisieren von Json zu abgeleiteten Typen in Asp.Net Web API"

Sie benötigen keinen benutzerdefinierten Modellbinder. Sie müssen auch nicht mit der Request-Pipeline herumalbern.

Werfen Sie einen Blick auf diese anderen SO: Wie benutzerdefinierte JsonConverter in JSON.NET zum Deserialisieren einer Liste von Basisklassenobjekten zu implementieren? .

Ich benutzte dies als Grundlage für meine eigene Lösung für das gleiche Problem.

Beginnend mit dem JsonCreationConverter auf das in diesem SO verwiesen wird (leicht modifiziert, um Probleme mit der Serialisierung von Typen in Antworten zu beheben):

 public abstract class JsonCreationConverter : JsonConverter { ///  /// this is very important, otherwise serialization breaks! ///  public override bool CanWrite { get { return false; } } ///  /// Create an instance of objectType, based properties in the JSON object ///  /// type of object expected /// contents of JSON object that will be /// deserialized ///  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(); } } 

Und jetzt können Sie Ihren Typ mit dem JsonConverterAttribute , indem Sie Json.Net auf einen benutzerdefinierten Konverter verweisen:

 [JsonConverter(typeof(MyCustomConverter))] public abstract class BaseClass{ private class MyCustomConverter : JsonCreationConverter { protected override BaseClass Create(Type objectType, Newtonsoft.Json.Linq.JObject jObject) { //TODO: read the raw JSON object through jObject to identify the type //eg here I'm reading a 'typename' property: if("DerivedType".Equals(jObject.Value("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; } } 

Jetzt können Sie den Basistyp als Parameter verwenden:

 public Result Post(BaseClass arg) { } 

Und wenn wir posten würden:

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

Dann wäre arg eine Instanz der DerivedClass , aber wenn wir gepostet haben:

 { DefaultProperty: 'world' } 

Dann erhalten Sie eine Instanz der DefaultClass .

EDIT – Warum bevorzuge ich diese Methode zu TypeNameHandling.Auto/All

Ich glaube, dass die Verwendung von TypeNameHandling.Auto/All die von JotaBe unterstützt wird, nicht immer die ideale Lösung ist. In diesem Fall mag es gut sein – aber persönlich werde ich es nicht tun, wenn:

  • Meine API wird nur von mir oder meinem Team verwendet werden
  • Es ist mir egal, einen doppelten XML-kompatiblen Endpunkt zu haben

Wenn Json.Net TypeNameHandling.Auto oder All verwendet wird, beginnt der Webserver mit dem Senden von MyNamespace.MyType, MyAssemblyName im Format MyNamespace.MyType, MyAssemblyName .

Ich habe in Kommentaren gesagt, dass dies ein Sicherheitsproblem ist. In einigen Dokumenten, die ich von Microsoft gelesen habe, wurde dies erwähnt. Es scheint nicht mehr erwähnt zu werden, aber ich denke immer noch, dass es eine berechtigte Sorge ist. Ich möchte Namespace-qualifizierte Typnamen und Assemblynamen niemals der Außenwelt offen legen. Es erhöht meine Angriffsfläche. Also, ja, ich kann keine Object / Parameter für meine API-Typen haben, aber wer sagt, dass der Rest meiner Site komplett lochfrei ist? Wer sagt, dass ein zukünftiger Endpunkt nicht die Möglichkeit bietet, Typnamen auszunutzen? Warum diese Chance nutzen, nur weil es einfacher ist?

Außerdem – wenn Sie eine “richtige” API schreiben, dh speziell für den Konsum von Drittanbietern und nicht nur für Sie selbst, und Sie Web-API verwenden, dann suchen Sie wahrscheinlich den JSON / XML-Inhaltstyp zu nutzen Handhabung (mindestens). Sehen Sie, wie weit Sie versuchen, leicht konsumierbare Dokumentation zu schreiben, die sich bei XML- und JSON-Formaten auf alle Ihre API-Typen bezieht.

Indem Sie überschreiben, wie JSON.Net die Typnamen versteht, können Sie die beiden in eine Linie bringen, indem Sie die Wahl zwischen XML / JSON für Ihren Aufrufer rein nach Geschmack treffen, anstatt dass die Typnamen in der einen oder anderen Art leichter zu merken sind.

Sie müssen es nicht selbst implementieren. JSON.NET bietet native Unterstützung dafür.

Sie müssen die gewünschte TypeNameHandling-Option für den JSON-Formatierer wie folgt angeben (im global.asax Anwendungsstart-Ereignis):

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

Wenn Sie Auto wie im obigen Beispiel angeben, wird der Parameter in den in der Eigenschaft $type des Objekts angegebenen Typ deserialisiert. Wenn die Eigenschaft $type fehlt, wird sie für den Parametertyp deserialisiert. Sie müssen den Typ nur angeben, wenn Sie einen Parameter eines abgeleiteten Typs übergeben. (Dies ist die flexibelste Option).

Wenn Sie diesen Parameter beispielsweise an eine Web-API-Aktion übergeben:

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

Der Parameter wird für ein Objekt der MyNamespace.MyType class deserialisiert.

Dies funktioniert auch für Untereigenschaften, dh Sie können ein Objekt wie dieses haben, das angibt, dass eine innere Eigenschaft einen bestimmten Typ hat

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

Hier sehen Sie ein Beispiel in der JSON.NET-Dokumentation von TypeNameHandling.Auto .

Dies funktioniert zumindest seit JSON.NET 4 Release .

HINWEIS

Sie müssen nichts mit Kleidung schmücken oder andere Anpassungen vornehmen. Es funktioniert ohne Änderungen in Ihrem Web-API-Code.

WICHTIGE NOTIZ

Der $ type muss die erste Eigenschaft des serialisierten JSON-Objekts sein . Wenn nicht, wird es ignoriert.

Vergleich zu benutzerdefinierten JsonConverter / JsonConverterAttribute

Ich vergleiche die native Lösung mit dieser Antwort .

So implementieren Sie den JsonConverter / JsonConverterAttribute :

  • Sie müssen einen benutzerdefinierten JsonConverter und ein benutzerdefiniertes JsonConverterAttribute
  • Sie müssen Attribute verwenden, um die Parameter zu markieren
  • Sie müssen vorher die möglichen Typen kennen, die für den Parameter erwartet werden
  • Sie müssen die Implementierung Ihres JsonConverter implementieren oder ändern, wenn sich Ihre Typen oder Eigenschaften ändern
  • Es gibt einen Code-Geruch von magischen Zeichenfolgen , um die erwarteten Eigenschaftsnamen anzuzeigen
  • Sie implementieren nicht etwas Generisches, das mit irgendeinem Typ verwendet werden kann
  • Du erfindest das Rad neu

Im Autor der Antwort gibt es einen Kommentar bezüglich der Sicherheit. Wenn Sie nicht etwas falsch machen (z. B. akzeptieren Sie einen zu generischen Typ für Ihren Parameter, wie Object ), besteht keine Gefahr, eine Instanz des falschen Typs zu erhalten: JSON.NET native Lösung instanziiert nur ein Objekt des Parametertyps oder einen abgeleiteten Typ daraus (wenn nicht, bekommst du null ).

Und das sind die Vorteile der nativen JSON.NET-Lösung:

  • Sie müssen nichts implementieren (Sie müssen das TypeNameHandling einmal in Ihrer App konfigurieren)
  • Sie müssen keine Attribute in Ihren Aktionsparametern verwenden
  • Sie müssen die möglichen Parametertypen nicht vorher kennen: Sie müssen lediglich den Basistyp kennen und ihn im Parameter angeben (es könnte ein abstrakter Typ sein, um Polymorphie deutlicher zu machen)
  • Die Lösung funktioniert in den meisten Fällen (1), ohne etwas zu ändern
  • Diese Lösung wird weitgehend getestet und optimiert
  • Du brauchst keine Zauberschnüre
  • Die Implementierung ist generisch und akzeptiert jeden abgeleiteten Typ

(1): Wenn Sie Parameterwerte erhalten möchten, die nicht vom selben Basistyp erben, funktioniert das nicht, aber ich sehe keinen Sinn darin

So kann ich keine Nachteile finden und viele Vorteile auf JSON.NET Lösung finden.

WARUM BENUTZERDEFINIERTE JsonConverter / JsonConverterAttribute

Dies ist eine gute funktionierende Lösung, die Anpassung ermöglicht, die geändert oder erweitert werden kann, um sie an Ihren speziellen Fall anzupassen.

Wenn Sie etwas tun möchten, das die native Lösung nicht ausführen kann, z. B. das Anpassen der Typnamen oder das Ableiten des Parametertyps anhand verfügbarer Eigenschaftsnamen, dann verwenden Sie diese Lösung für Ihren eigenen Fall. Der andere kann nicht angepasst werden und funktioniert nicht für Ihre Bedürfnisse.

Sie können asynchrone Methoden normal aufrufen, Ihre Ausführung wird einfach ausgesetzt, bis die Methode zurückkehrt und Sie das Modell in standardmäßiger Weise zurückgeben können. Mach einfach einen Anruf wie folgt:

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

Es wird dir rohes JSON geben.

Wenn Sie das TypeNameHandling.Auto verwenden möchten, aber sich mit der Sicherheit befassen oder nicht mögen, dass api Verbraucher dieses Niveau von hinter den Kulissenwissen benötigen, können Sie den $ type deserialize Ihr Selbst behandeln.

 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); } } } 

Dann hakt das in global.asax.Application__Start

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

Schließlich habe ich eine Wrapper-class und [JsonProperty (TypeNameHandling = TypeNameHandling.Auto)] auf einer properpty verwendet, die das Objekt mit verschiedenen Typen enthält, da ich es nicht durch die Konfiguration der tatsächlichen class zum functionieren gebracht habe.

Dieser Ansatz ermöglicht es den Verbrauchern, die benötigten Informationen in ihre Anfrage aufzunehmen, während die Dokumentation der zulässigen Werte plattformunabhängig, einfach zu ändern und leicht zu verstehen ist. Alles ohne einen eigenen Gesprächspartner schreiben zu müssen.

Kredit an: https://mallibone.com/post/serialize-object-inheritance-with-json.net, um mir den benutzerdefinierten Deserializer dieser Feldeigenschaft anzuzeigen.