web-dev-qa-db-fra.com

Désérialisation de Json en types dérivés dans l'API Web Asp.Net

J'appelle une méthode de mon WebAPI envoyant un json que je voudrais faire correspondre (ou lier) avec un modèle.

Dans le contrôleur, j'ai une méthode comme:

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

'MyClass', qui est donné comme paramètre est une classe abstraite. J'aimerais que, selon le type de json passé, la classe héritée correcte soit instanciée.

Pour y parvenir, j'essaie d'implémenter un classeur personnalisé. Le problème est que (je ne sais pas si c'est très basique mais je ne trouve rien) je ne sais pas comment récupérer le Json brut (ou mieux, une sorte de sérialisation) qui vient dans la demande.

Je vois:

  • actionContext.Request.Content

Mais toutes les méthodes sont exposées comme asynchrones. Je ne sais pas à qui cela correspond avec le passage du modèle de génération à la méthode du contrôleur ...

Merci beaucoup!

59
IoChaos

Vous n'avez pas besoin d'un classeur de modèle personnalisé. Vous n'avez pas non plus besoin de vous occuper du pipeline de demandes.

Jetez un oeil à cet autre SO: Comment implémenter JsonConverter personnalisé dans JSON.NET pour désérialiser une liste d'objets de classe de base? .

J'ai utilisé cela comme base pour ma propre solution au même problème.

Commençant par le JsonCreationConverter<T> référencé en cela SO (légèrement modifié pour résoudre les problèmes de sérialisation des types dans les réponses):

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

Et maintenant, vous pouvez annoter votre type avec le JsonConverterAttribute, pointant Json.Net vers un convertisseur personnalisé:

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

Vous pouvez maintenant utiliser le type de base comme paramètre:

public Result Post(BaseClass arg) {

}

Et si nous devions poster:

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

Alors arg serait une instance de DerivedClass, mais si nous postions:

{ DefaultProperty: 'world' }

Vous obtiendrez alors une instance de DefaultClass.

EDIT - Pourquoi je préfère cette méthode à TypeNameHandling.Auto/All

Je crois que l'utilisation du TypeNameHandling.Auto/All adopté par JotaBe n'est pas toujours la solution idéale. C'est peut-être le cas dans ce cas - mais personnellement, je ne le ferai que si:

  • Mon API est seulement jamais va être utilisée par moi ou mon équipe
  • Je ne me soucie pas d'avoir un point de terminaison compatible XML double

Lorsque Json.Net TypeNameHandling.Auto ou All sont utilisés, votre serveur Web commencera à envoyer des noms de type au format MyNamespace.MyType, MyAssemblyName.

J'ai dit dans les commentaires que je pense que c'est une préoccupation de sécurité. Cela a été mentionné dans certaines documentations que j'ai lues de Microsoft. Ce n'est plus mentionné, semble-t-il, mais je pense toujours que c'est une préoccupation valable. Je ne veux pas jamais vouloir exposer les noms de types qualifiés par les espaces de noms et les noms d'assemblages au monde extérieur. Cela augmente ma surface d'attaque. Donc, oui, je ne peux pas avoir Object propriétés/paramètres mes types d'API, mais qui peut dire que le reste de mon site est complètement sans trous? Qui peut dire qu'un futur point de terminaison n'expose pas la possibilité d'exploiter les noms de type? Pourquoi prendre cette chance juste parce que c'est plus facile?

Aussi - si vous écrivez une API "appropriée", c'est-à-dire spécifiquement pour la consommation par des tiers et pas seulement pour vous-même, et que vous utilisez l'API Web, vous cherchez probablement à tirer parti du type de contenu JSON/XML manipulation (au minimum). Voyez jusqu'où vous essayez d'écrire une documentation facile à consommer, qui fait référence à tous vos types d'API différemment pour les formats XML et JSON.

En remplaçant la façon dont JSON.Net comprend les noms de type, vous pouvez aligner les deux, en faisant le choix entre XML/JSON pour votre appelant uniquement en fonction des goûts, plutôt que parce que les noms de type sont plus faciles à mémoriser dans l'un ou l'autre.

89
Andras Zoltan

Vous n'avez pas besoin de l'implémenter vous-même. JSON.NET a un support natif pour cela.

Vous devez spécifier option TypeNameHandling souhaitée pour le formateur JSON, comme ceci (dans global.asax événement de démarrage d'application):

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

Si vous spécifiez Auto, comme dans l'exemple ci-dessus, le paramètre sera désérialisé au type spécifié dans $type propriété de l'objet. Si la $type la propriété est manquante, elle sera désérialisée au type du paramètre. Il vous suffit donc de spécifier le type lorsque vous passez un paramètre d'un type dérivé. (C'est l'option la plus flexible).

Par exemple, si vous transmettez ce paramètre à une action d'API Web:

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

Le paramètre sera désérialisé en un objet de MyNamespace.MyType classe.

Cela fonctionne également pour les sous-propriétés, c'est-à-dire que vous pouvez avoir un objet comme celui-ci, qui spécifie qu'une propriété interne est d'un type donné

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

Ici vous pouvez voir un exemple sur la documentation JSON.NET de TypeNameHandling.Auto .

Cela fonctionne au moins depuis la version JSON.NET 4 .

[~ # ~] note [~ # ~]

Vous n'avez pas besoin de décorer quoi que ce soit avec des attirbutes, ou de faire toute autre personnalisation. Cela fonctionnera sans aucune modification dans votre code d'API Web.

REMARQUE IMPORTANTE

Le type $ doit être la première propriété de l'objet sérialisé JSON . Sinon, il sera ignoré.

COMPARAISON AVEC JsonConverter/JsonConverterAttribute CUSTOM

Je compare la solution native à cette réponse .

Pour implémenter le JsonConverter/JsonConverterAttribute:

  • vous devez implémenter un JsonConverter personnalisé et un JsonConverterAttribute personnalisé
  • vous devez utiliser des attributs pour marquer les paramètres
  • vous devez connaître à l'avance les types possibles attendus pour le paramètre
  • vous devez implémenter ou modifier l'implémentation de votre JsonConverter chaque fois que vos types ou propriétés changent
  • il y a une odeur de code chaînes magiques , pour indiquer les noms de propriété attendus
  • vous n'implémentez pas quelque chose de générique qui peut être utilisé avec n'importe quel type
  • tu réinventes la roue

Dans l'auteur de la réponse, il y a un commentaire concernant la sécurité. À moins que vous ne fassiez quelque chose de mal (comme accepter un type trop générique pour votre paramètre, comme Object), il n'y a aucun risque d'obtenir une instance du mauvais type: la solution native JSON.NET instancie uniquement un objet du type du paramètre , ou un type qui en dérive (sinon, vous obtenez null).

Et ce sont les avantages de la solution native JSON.NET:

  • vous n'avez pas besoin d'implémenter quoi que ce soit (il vous suffit de configurer le TypeNameHandling une fois dans votre application)
  • vous n'avez pas besoin d'utiliser d'attributs dans vos paramètres d'action
  • vous n'avez pas besoin de connaître les types de paramètres possibles à l'avance: il vous suffit de connaître le type de base et de le spécifier dans le paramètre (il pourrait s'agir d'un type abstrait, pour rendre le polymorphisme plus évident)
  • la solution fonctionne pour la plupart des cas (1) sans rien changer
  • cette solution est largement testée et optimisée
  • vous n'avez pas besoin de cordes magiques
  • l'implémentation est générique et accepte tout type dérivé

(1): si vous voulez recevoir des valeurs de paramètres qui n'héritent pas du même type de base, cela ne fonctionnera pas, mais je ne vois aucun intérêt à le faire

Je ne trouve donc aucun inconvénient et trouve de nombreux avantages sur la solution JSON.NET.

POURQUOI UTILISER CUSTOM JsonConverter/JsonConverterAttribute

Il s'agit d'une bonne solution de travail qui permet la personnalisation, qui peut être modifiée ou étendue pour l'adapter à votre cas particulier.

Si vous voulez faire quelque chose que la solution native ne peut pas faire, comme personnaliser les noms de type ou déduire le type du paramètre en fonction des noms de propriété disponibles, utilisez cette solution adaptée à votre cas. L'autre ne peut pas être personnalisé et ne fonctionnera pas selon vos besoins.

49
JotaBe

Vous pouvez appeler des méthodes asynchrones normalement, votre exécution sera simplement suspendue jusqu'à ce que la méthode revienne et vous pouvez renvoyer le modèle de manière standard. Appelez simplement comme ceci:

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

Il vous donnera un JSON brut.

4
tpeczek

Si vous souhaitez utiliser TypeNameHandling.Auto mais que vous êtes préoccupé par la sécurité ou que vous n'aimez pas les consommateurs d'api ayant besoin de ce niveau de connaissances en coulisses, vous pouvez gérer le type $ désérialisez vous-même.

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

Ensuite, connectez cela dans global.asax.Application__Start

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

enfin, j'ai utilisé une classe wrapper et [JsonProperty (TypeNameHandling = TypeNameHandling.Auto)] sur un properpty contenant l'objet avec différents types car je n'ai pas pu le faire fonctionner en configurant la classe réelle.

Cette approche permet aux consommateurs d'inclure les informations nécessaires dans leur demande tout en permettant à la documentation des valeurs autorisées d'être indépendante de la plate-forme, facile à modifier et facile à comprendre. Le tout sans avoir à écrire votre propre converseur.

Nous remercions: https://mallibone.com/post/serialize-object-inheritance-with-json.net de m'avoir montré le désérialiseur personnalisé de cette propriété de champ.

2
user8606451