Interfaces de diffusion pour la désérialisation dans JSON.NET

129

J'essaie de configurer un lecteur qui prendra des objets JSON de divers sites Web (pensez à la récupération d'informations) et les traduira en objets C #. J'utilise actuellement JSON.NET pour le processus de désérialisation. Le problème que je rencontre est qu'il ne sait pas comment gérer les propriétés au niveau de l'interface dans une classe. Donc quelque chose de la nature:

public IThingy Thing

Produira l'erreur:

Impossible de créer une instance de type IThingy. Le type est une interface ou une classe abstraite et ne peut pas être instancié.

Il est relativement important que ce soit un IThingy plutôt qu'un Thingy car le code sur lequel je travaille est considéré comme sensible et les tests unitaires sont très importants. La simulation d'objets pour les scripts de test atomique n'est pas possible avec des objets à part entière comme Thingy. Ils doivent être une interface.

Je me penche sur la documentation de JSON.NET depuis un moment maintenant, et les questions que j'ai pu trouver sur ce site à ce sujet remontent toutes à plus d'un an. De l'aide?

De plus, si cela compte, mon application est écrite en .NET 4.0.

tmesser
la source

Réponses:

115

@SamualDavis a fourni une excellente solution dans une question connexe , que je résumerai ici.

Si vous devez désérialiser un flux JSON dans une classe concrète qui a des propriétés d'interface, vous pouvez inclure les classes concrètes en tant que paramètres d'un constructeur pour la classe! Le désérialiseur NewtonSoft est suffisamment intelligent pour comprendre qu'il doit utiliser ces classes concrètes pour désérialiser les propriétés.

Voici un exemple:

public class Visit : IVisit
{
    /// <summary>
    /// This constructor is required for the JSON deserializer to be able
    /// to identify concrete classes to use when deserializing the interface properties.
    /// </summary>
    public Visit(MyLocation location, Guest guest)
    {
        Location = location;
        Guest = guest;
    }
    public long VisitId { get; set; }
    public ILocation Location { get;  set; }
    public DateTime VisitDate { get; set; }
    public IGuest Guest { get; set; }
}
Mark Meuer
la source
15
Comment cela fonctionnerait-il avec une ICollection? ICollection <IGuest> Invités {get; set;}
DrSammyD
12
Cela fonctionne avec ICollection <ConcreteClass>, donc ICollection <Guest> fonctionne. Tout comme un FYI, vous pouvez mettre l'attribut [JsonConstructor] sur votre constructeur afin qu'il l'utilise par défaut si vous avez plusieurs constructeurs
DrSammyD
6
Je suis coincé dans le même problème, dans mon cas, j'ai plusieurs implémentations de l'interface (dans votre exemple, l'interface est ILocation) alors que se passe-t-il s'il y a des classes comme MyLocation, VIPLocation, OrdinaryLocation. Comment les mapper à la propriété Location? Si vous n'avez qu'une seule implémentation comme MyLocation, c'est facile, mais comment le faire s'il existe plusieurs implémentations d'ILocation?
ATHER
10
Si vous avez plus d'un constructeur, vous pouvez baliser votre constructeur spécial avec l' [JsonConstructor]attribut.
Dr Rob Lang
26
Cela ne va pas du tout. Le but d'utiliser des interfaces est d'utiliser l'injection de dépendances, mais en faisant cela avec un paramètre de type objet requis par votre constructeur, vous foutez totalement le point d'avoir une interface comme propriété.
Jérôme MEVEL
57

(Copié de cette question )

Dans les cas où je n'ai pas eu de contrôle sur le JSON entrant (et ne peux donc pas m'assurer qu'il inclut une propriété $ type), j'ai écrit un convertisseur personnalisé qui vous permet simplement de spécifier explicitement le type concret:

public class Model
{
    [JsonConverter(typeof(ConcreteTypeConverter<Something>))]
    public ISomething TheThing { get; set; }
}

Cela utilise simplement l'implémentation du sérialiseur par défaut de Json.Net tout en spécifiant explicitement le type concret.

Un aperçu est disponible sur ce billet de blog . Le code source est ci-dessous:

public class ConcreteTypeConverter<TConcrete> : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        //assume we can convert to anything for now
        return true;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        //explicitly specify the concrete type we want to create
        return serializer.Deserialize<TConcrete>(reader);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        //use the default serialization - it works fine
        serializer.Serialize(writer, value);
    }
}
Steve Greatrex
la source
11
J'aime vraiment cette approche et je l'ai appliquée à notre propre projet. J'ai même ajouté un ConcreteListTypeConverter<TInterface, TImplementation>pour gérer les membres de classe de type IList<TInterface>.
Oliver
3
C'est un bon morceau de code. Il serait peut-être plus agréable d'avoir le code réel pour concreteTypeConverterla question.
Chris
2
@Oliver - Pouvez-vous publier votre ConcreteListTypeConverter<TInterface, TImplementation>implémentation?
Michael
2
Et si vous avez deux implémenteurs d'ISomething?
bdaniel7
56

Pourquoi utiliser un convertisseur? Il existe une fonctionnalité native Newtonsoft.Jsonpour résoudre ce problème exact:

Situé TypeNameHandlingdans le JsonSerializerSettingsàTypeNameHandling.Auto

JsonConvert.SerializeObject(
  toSerialize,
  new JsonSerializerSettings()
  {
    TypeNameHandling = TypeNameHandling.Auto
  });

Cela mettra chaque type dans le json, qui n'est pas considéré comme une instance concrète d'un type mais comme une interface ou une classe abstraite.

Assurez-vous que vous utilisez les mêmes paramètres pour la sérialisation et la désérialisation .

Je l'ai testé, et cela fonctionne à merveille, même avec des listes.

Résultats de la recherche Résultat Web avec liens vers des sites

⚠️ AVERTISSEMENT :

Utilisez-le uniquement pour json à partir d'une source connue et fiable. L'utilisateur snipsnipsnip a correctement mentionné qu'il s'agit en effet d'une vunerability.

Voir CA2328 et SCS0028 pour plus d'informations.


Source et une implémentation manuelle alternative: Code Inside Blog

Mafii
la source
3
Parfait, cela m'a aidé pour un clone profond rapide et sale ( stackoverflow.com/questions/78536/deep-cloning-objects )
Compufreak
1
@Shimmy Objects: "Inclure le nom du type .NET lors de la sérialisation dans une structure d'objet JSON." Auto: incluez le nom du type .NET lorsque le type de l'objet sérialisé n'est pas le même que son type déclaré. Notez que cela n'inclut pas l'objet sérialisé racine par défaut. Pour inclure le nom de type de l'objet racine dans JSON, vous devez spécifier un objet de type racine avec SerializeObject (Object, Type, JsonSerializerSettings) ou Serialize (JsonWriter, Object, Type). "Source: newtonsoft.com/json/help/html/…
Mafii
4
J'ai juste essayé ceci sur la désérialisation et cela ne fonctionne pas. La ligne d'objet de cette question Stack Overflow est: "Casting des interfaces pour la désérialisation dans JSON.NET"
Justin Russo
3
@JustinRusso cela ne fonctionne que lorsque le json a été sérialisé avec le même paramètre
Mafii
3
Votez pour la solution rapide, sinon sale. Si vous ne faites que sérialiser des configurations, cela fonctionne. Beats arrêter le développement pour construire des convertisseurs et certainement battre la décoration de chaque propriété injectée. serializer.TypeNameHandling = TypeNameHandling.Auto; JsonConvert.DefaultSettings (). TypeNameHandling = TypeNameHandling.Auto;
Sean Anderson
39

Pour activer la désérialisation de plusieurs implémentations d'interfaces, vous pouvez utiliser JsonConverter, mais pas via un attribut:

Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer();
serializer.Converters.Add(new DTOJsonConverter());
Interfaces.IEntity entity = serializer.Deserialize(jsonReader);

DTOJsonConverter mappe chaque interface avec une implémentation concrète:

class DTOJsonConverter : Newtonsoft.Json.JsonConverter
{
    private static readonly string ISCALAR_FULLNAME = typeof(Interfaces.IScalar).FullName;
    private static readonly string IENTITY_FULLNAME = typeof(Interfaces.IEntity).FullName;


    public override bool CanConvert(Type objectType)
    {
        if (objectType.FullName == ISCALAR_FULLNAME
            || objectType.FullName == IENTITY_FULLNAME)
        {
            return true;
        }
        return false;
    }

    public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
    {
        if (objectType.FullName == ISCALAR_FULLNAME)
            return serializer.Deserialize(reader, typeof(DTO.ClientScalar));
        else if (objectType.FullName == IENTITY_FULLNAME)
            return serializer.Deserialize(reader, typeof(DTO.ClientEntity));

        throw new NotSupportedException(string.Format("Type {0} unexpected.", objectType));
    }

    public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }
}

DTOJsonConverter n'est requis que pour le désérialiseur. Le processus de sérialisation est inchangé. L'objet Json n'a pas besoin d'incorporer des noms de types concrets.

Ce poste SO offre la même solution un peu plus loin avec un JsonConverter générique.

Eric Boumendil
la source
L'appel de cette méthode WriteJson à serializer.Serialize ne provoquerait-il pas un débordement de pile, car l'appel de serialize sur la valeur sérialisée par le convertisseur entraînerait l'appel de la méthode WriteJson du convertisseur de manière récursive?
Triynko
Il ne devrait pas, si la méthode CanConvert () renvoie un résultat cohérent.
Eric Boumendil
3
Pourquoi comparez-vous des FullNames alors que vous pouvez simplement comparer des types directement?
Alex Zhukovskiy
La simple comparaison des types est également très bien.
Eric Boumendil
23

Utilisez cette classe, pour mapper le type abstrait au type réel:

public class AbstractConverter<TReal, TAbstract> : JsonConverter where TReal : TAbstract
{
    public override Boolean CanConvert(Type objectType) 
        => objectType == typeof(TAbstract);

    public override Object ReadJson(JsonReader reader, Type type, Object value, JsonSerializer jser) 
        => jser.Deserialize<TReal>(reader);

    public override void WriteJson(JsonWriter writer, Object value, JsonSerializer jser) 
        => jser.Serialize(writer, value);
}

... et lors de la désérialisation:

        var settings = new JsonSerializerSettings
        {
            Converters = {
                new AbstractConverter<Thing, IThingy>(),
                new AbstractConverter<Thing2, IThingy2>()
            },
        };

        JsonConvert.DeserializeObject(json, type, settings);
Gildor
la source
1
J'aime vraiment une belle réponse concise qui résout mon problème. Pas besoin d'autofac ou quoi que ce soit!
Ben Power
3
Cela vaut la peine de mettre ceci à la déclaration de classe de convertisseur: where TReal : TAbstractpour vous assurer qu'il peut lancer le type
Artemious
1
Un où pourrait être plus complet where TReal : class, TAbstract, new().
Erik Philips
2
J'ai aussi utilisé ce convertisseur avec struct, je pense que "où TReal: TAbstract" est suffisant. Merci a tous.
Gildor
2
Or! Une façon élégante d'aller.
SwissCoder
12

Nicholas Westby a fourni une excellente solution dans un article génial .

Si vous souhaitez désérialiser JSON vers l'une des nombreuses classes possibles qui implémentent une interface comme celle-ci:

public class Person
{
    public IProfession Profession { get; set; }
}

public interface IProfession
{
    string JobTitle { get; }
}

public class Programming : IProfession
{
    public string JobTitle => "Software Developer";
    public string FavoriteLanguage { get; set; }
}

public class Writing : IProfession
{
    public string JobTitle => "Copywriter";
    public string FavoriteWord { get; set; }
}

public class Samples
{
    public static Person GetProgrammer()
    {
        return new Person()
        {
            Profession = new Programming()
            {
                FavoriteLanguage = "C#"
            }
        };
    }
}

Vous pouvez utiliser un convertisseur JSON personnalisé:

public class ProfessionConverter : JsonConverter
{
    public override bool CanWrite => false;
    public override bool CanRead => true;
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(IProfession);
    }
    public override void WriteJson(JsonWriter writer,
        object value, JsonSerializer serializer)
    {
        throw new InvalidOperationException("Use default serialization.");
    }

    public override object ReadJson(JsonReader reader,
        Type objectType, object existingValue,
        JsonSerializer serializer)
    {
        var jsonObject = JObject.Load(reader);
        var profession = default(IProfession);
        switch (jsonObject["JobTitle"].Value())
        {
            case "Software Developer":
                profession = new Programming();
                break;
            case "Copywriter":
                profession = new Writing();
                break;
        }
        serializer.Populate(jsonObject.CreateReader(), profession);
        return profession;
    }
}

Et vous devrez décorer la propriété "Profession" avec un attribut JsonConverter pour lui faire savoir d'utiliser votre convertisseur personnalisé:

    public class Person
    {
        [JsonConverter(typeof(ProfessionConverter))]
        public IProfession Profession { get; set; }
    }

Et puis, vous pouvez diffuser votre classe avec une interface:

Person person = JsonConvert.DeserializeObject<Person>(jsonString);
A. Morel
la source
8

Deux choses que vous pourriez essayer:

Implémentez un modèle try / parse:

public class Organisation {
  public string Name { get; set; }

  [JsonConverter(typeof(RichDudeConverter))]
  public IPerson Owner { get; set; }
}

public interface IPerson {
  string Name { get; set; }
}

public class Tycoon : IPerson {
  public string Name { get; set; }
}

public class Magnate : IPerson {
  public string Name { get; set; }
  public string IndustryName { get; set; }
}

public class Heir: IPerson {
  public string Name { get; set; }
  public IPerson Benefactor { get; set; }
}

public class RichDudeConverter : JsonConverter
{
  public override bool CanConvert(Type objectType)
  {
    return (objectType == typeof(IPerson));
  }

  public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
  {
    // pseudo-code
    object richDude = serializer.Deserialize<Heir>(reader);

    if (richDude == null)
    {
        richDude = serializer.Deserialize<Magnate>(reader);
    }

    if (richDude == null)
    {
        richDude = serializer.Deserialize<Tycoon>(reader);
    }

    return richDude;
  }

  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
  {
    // Left as an exercise to the reader :)
    throw new NotImplementedException();
  }
}

Ou, si vous pouvez le faire dans votre modèle objet, implémentez une classe de base concrète entre IPerson et vos objets feuille, et désérialisez-la.

Le premier peut potentiellement échouer à l'exécution, le second nécessite des modifications de votre modèle d'objet et homogénéise la sortie au plus petit dénominateur commun.

mcw
la source
Un modèle try / parse n'est pas faisable en raison de l'échelle avec laquelle je dois travailler. Je dois considérer une portée de centaines d'objets de base avec encore plus de centaines d'objets stub / helper pour représenter les objets JSON incorporés qui se produisent souvent. Il n'est pas hors de question de changer le modèle d'objet, mais l'utilisation d'une classe de base concrète dans les propriétés ne nous empêcherait-elle pas de simuler des éléments pour les tests unitaires? Ou est-ce que je reçois cela en arrière?
tmesser
Vous pouvez toujours implémenter une maquette d'IPerson - notez que le type de la propriété Organisation.Owner est toujours IPerson. Mais pour la désérialisation d'une cible arbitraire, vous devez renvoyer un type concret. Si vous ne possédez pas la définition de type et que vous ne pouvez pas définir l'ensemble minimum de propriétés dont votre code aura besoin, votre dernier recours est quelque chose comme un sac clé / valeur. En utilisant votre exemple de commentaire sur Facebook - pouvez-vous publier dans une réponse à quoi ressemblent vos (une ou plusieurs) implémentations d'ILocation? Cela peut aider à faire avancer les choses.
mcw
Puisque le principal espoir est moqueur, l'interface ILocation n'est en réalité qu'une façade pour l'objet concret Location. Un exemple rapide que je viens de travailler serait quelque chose comme ceci ( pastebin.com/mWQtqGnB ) pour l'interface et ceci ( pastebin.com/TdJ6cqWV ) pour l'objet concret.
tmesser
Et pour passer à l'étape suivante, voici un exemple de ce à quoi ressemblerait IPage ( pastebin.com/iuGifQXp ) et Page ( pastebin.com/ebqLxzvm ). Le problème, bien sûr, étant que bien que la désérialisation de Page fonctionne généralement correctement, elle s'étouffera lorsqu'elle atteindra la propriété ILocation.
tmesser
Ok, alors en pensant aux objets que vous grattez et désérialisez réellement - est-il généralement vrai que les données JSON sont cohérentes avec une seule définition de classe concrète? Cela signifie-t-il (hypothétiquement) que vous ne rencontreriez pas des «emplacements» avec des propriétés supplémentaires qui rendraient Location impropre à l'utilisation comme type concret pour l'objet désérialisé? Si tel est le cas, l'attribution de la propriété ILocation de Page avec un "LocationConverter" devrait fonctionner. Sinon, et c'est parce que les données JSON ne sont pas toujours conformes à une structure rigide ou cohérente (comme ILocation), alors (... suite)
mcw
8

J'ai trouvé cela utile. Vous pourriez aussi.

Exemple d'utilisation

public class Parent
{
    [JsonConverter(typeof(InterfaceConverter<IChildModel, ChildModel>))]
    IChildModel Child { get; set; }
}

Convertisseur de création personnalisée

public class InterfaceConverter<TInterface, TConcrete> : CustomCreationConverter<TInterface>
    where TConcrete : TInterface, new()
{
    public override TInterface Create(Type objectType)
    {
        return new TConcrete();
    }
}

Documentation Json.NET

Smiggleworth
la source
1
Pas une solution viable. N'aborde pas les listes et conduit à saupoudrer des décorateurs / annotations partout.
Sean Anderson
5

Pour ceux qui pourraient être curieux de connaître le ConcreteListTypeConverter qui a été référencé par Oliver, voici ma tentative:

public class ConcreteListTypeConverter<TInterface, TImplementation> : JsonConverter where TImplementation : TInterface 
{
    public override bool CanConvert(Type objectType)
    {
        return true;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var res = serializer.Deserialize<List<TImplementation>>(reader);
        return res.ConvertAll(x => (TInterface) x);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }
}
Matt M
la source
1
Je suis confus avec le surpassé CanConvert(Type objectType) { return true;}. Cela semble piraté, en quoi cela est-il utile? Je me trompe peut-être, mais n'est-ce pas comme dire à un petit combattant inexpérimenté qu'il va gagner le combat, peu importe l'adversaire?
Chef_Code
4

Pour ce que ça vaut, j'ai fini par devoir gérer cela moi-même pour la plupart. Chaque objet a une méthode Deserialize (string jsonStream) . Quelques extraits de celui-ci:

JObject parsedJson = this.ParseJson(jsonStream);
object thingyObjectJson = (object)parsedJson["thing"];
this.Thing = new Thingy(Convert.ToString(thingyObjectJson));

Dans ce cas, new Thingy (string) est un constructeur qui appellera la méthode Deserialize (string jsonStream) du type concret approprié. Ce schéma continuera à descendre et à descendre jusqu'à ce que vous atteigniez les points de base que json.NET peut simplement gérer.

this.Name = (string)parsedJson["name"];
this.CreatedTime = DateTime.Parse((string)parsedJson["created_time"]);

Etc., etc. Cette configuration m'a permis de donner des configurations json.NET qu'il peut gérer sans avoir à refactoriser une grande partie de la bibliothèque elle-même ou à utiliser des modèles try / parse peu maniables qui auraient embourbé toute notre bibliothèque en raison du nombre d'objets impliqués. Cela signifie également que je peux gérer efficacement toutes les modifications json sur un objet spécifique et que je n'ai pas besoin de m'inquiéter de tout ce que cet objet touche. Ce n'est en aucun cas la solution idéale, mais elle fonctionne assez bien à partir de nos tests unitaires et d'intégration.

tmesser
la source
4

Supposons un paramètre autofac comme celui-ci:

public class AutofacContractResolver : DefaultContractResolver
{
    private readonly IContainer _container;

    public AutofacContractResolver(IContainer container)
    {
        _container = container;
    }

    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        JsonObjectContract contract = base.CreateObjectContract(objectType);

        // use Autofac to create types that have been registered with it
        if (_container.IsRegistered(objectType))
        {
           contract.DefaultCreator = () => _container.Resolve(objectType);
        }  

        return contract;
    }
}

Ensuite, supposons que votre classe ressemble à ceci:

public class TaskController
{
    private readonly ITaskRepository _repository;
    private readonly ILogger _logger;

    public TaskController(ITaskRepository repository, ILogger logger)
    {
        _repository = repository;
        _logger = logger;
    }

    public ITaskRepository Repository
    {
        get { return _repository; }
    }

    public ILogger Logger
    {
        get { return _logger; }
    }
}

Par conséquent, l'utilisation du résolveur dans la désérialisation pourrait être comme:

ContainerBuilder builder = new ContainerBuilder();
builder.RegisterType<TaskRepository>().As<ITaskRepository>();
builder.RegisterType<TaskController>();
builder.Register(c => new LogService(new DateTime(2000, 12, 12))).As<ILogger>();

IContainer container = builder.Build();

AutofacContractResolver contractResolver = new AutofacContractResolver(container);

string json = @"{
      'Logger': {
        'Level':'Debug'
      }
}";

// ITaskRespository and ILogger constructor parameters are injected by Autofac 
TaskController controller = JsonConvert.DeserializeObject<TaskController>(json, new JsonSerializerSettings
{
    ContractResolver = contractResolver
});

Console.WriteLine(controller.Repository.GetType().Name);

Vous pouvez voir plus de détails sur http://www.newtonsoft.com/json/help/html/DeserializeWithDependencyInjection.htm

OMG
la source
Je voterai pour cela comme la meilleure solution. DI a été si largement utilisé ces jours-ci par les développeurs web c #, et cela convient parfaitement comme un endroit centralisé pour gérer ces conversions de type par le résolveur.
appletwo
3

Aucun objet ne jamais être un IThingy comme interfaces sont abstraites par définition.

L'objet que vous avez qui a été sérialisé pour la première fois était d'un type concret , implémentant l' interface abstraite . Vous devez avoir ce même béton classe raviver les données sérialisées.

L'objet résultant sera alors d'un type qui implémente l' interface abstraite que vous recherchez.

De la documentation, il suit que vous pouvez utiliser

(Thingy)JsonConvert.DeserializeObject(jsonString, typeof(Thingy));

lors de la désérialisation pour informer JSON.NET du type concret.

Sean Kinsey
la source
C'est précisément le message d'il y a plus d'un an dont je parlais. La seule suggestion majeure (écrire des convertisseurs personnalisés) n'est pas vraiment réalisable avec l'échelle que je suis obligé de considérer. JSON.NET a beaucoup changé au cours de cette année. Je comprends parfaitement la distinction entre une classe et une interface, mais C # prend également en charge les conversions implicites d'une interface à un objet qui implémente l'interface en ce qui concerne la saisie. Je demande essentiellement s'il existe un moyen de dire à JSON.NET quel objet implémentera cette interface.
tmesser
Tout était là dans la réponse que je vous ai indiquée. Assurez-vous qu'il existe une _typepropriété qui indique le type de béton à utiliser.
Sean Kinsey
Et je doute fortement que C # prenne en charge tout type de transtypage «implicite» d'une variable déclarée en tant qu'interface vers un type concret sans aucune sorte d'indices.
Sean Kinsey
Sauf si je l'ai mal lu, la propriété _type était censée être dans le JSON pour être sérialisée. Cela fonctionne bien si vous ne désérialisez que ce que vous avez déjà sérialisé, mais ce n'est pas ce qui se passe ici. Je tire JSON d'un certain nombre de sites qui ne suivront pas cette norme.
tmesser
@YYY - Contrôlez-vous à la fois la sérialisation et la désérialisation du JSON source? Parce qu'en fin de compte, vous devrez soit intégrer le type concret dans le JSON sérialisé comme indice à utiliser lors de la désérialisation, soit utiliser une sorte de modèle try / parse qui détecte / tente de détecter le type concret au moment de l'exécution et appelez le désérialiseur approprié.
mcw
3

Ma solution à celle-ci, que j'aime car elle est bien générale, est la suivante:

/// <summary>
/// Automagically convert known interfaces to (specific) concrete classes on deserialisation
/// </summary>
public class WithMocksJsonConverter : JsonConverter
{
    /// <summary>
    /// The interfaces I know how to instantiate mapped to the classes with which I shall instantiate them, as a Dictionary.
    /// </summary>
    private readonly Dictionary<Type,Type> conversions = new Dictionary<Type,Type>() { 
        { typeof(IOne), typeof(MockOne) },
        { typeof(ITwo), typeof(MockTwo) },
        { typeof(IThree), typeof(MockThree) },
        { typeof(IFour), typeof(MockFour) }
    };

    /// <summary>
    /// Can I convert an object of this type?
    /// </summary>
    /// <param name="objectType">The type under consideration</param>
    /// <returns>True if I can convert the type under consideration, else false.</returns>
    public override bool CanConvert(Type objectType)
    {
        return conversions.Keys.Contains(objectType);
    }

    /// <summary>
    /// Attempt to read an object of the specified type from this reader.
    /// </summary>
    /// <param name="reader">The reader from which I read.</param>
    /// <param name="objectType">The type of object I'm trying to read, anticipated to be one I can convert.</param>
    /// <param name="existingValue">The existing value of the object being read.</param>
    /// <param name="serializer">The serializer invoking this request.</param>
    /// <returns>An object of the type into which I convert the specified objectType.</returns>
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        try
        {
            return serializer.Deserialize(reader, this.conversions[objectType]);
        }
        catch (Exception)
        {
            throw new NotSupportedException(string.Format("Type {0} unexpected.", objectType));
        }
    }

    /// <summary>
    /// Not yet implemented.
    /// </summary>
    /// <param name="writer">The writer to which I would write.</param>
    /// <param name="value">The value I am attempting to write.</param>
    /// <param name="serializer">the serializer invoking this request.</param>
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

}

Vous pouvez évidemment et trivialement le convertir en un convertisseur encore plus général en ajoutant un constructeur qui prend un argument de type Dictionary <Type, Type> avec lequel instancier la variable d'instance de conversions.

Simon Brooke
la source
3

Plusieurs années plus tard, j'ai eu un problème similaire. Dans mon cas, il y avait des interfaces fortement imbriquées et une préférence pour la génération des classes concrètes au moment de l'exécution afin que cela fonctionne avec une classe générique.

J'ai décidé de créer une classe proxy au moment de l'exécution qui encapsule l'objet retourné par Newtonsoft.

L'avantage de cette approche est qu'elle ne nécessite pas d'implémentation concrète de la classe et peut gérer automatiquement n'importe quelle profondeur d'interfaces imbriquées. Vous pouvez en voir plus sur mon blog .

using Castle.DynamicProxy;
using Newtonsoft.Json.Linq;
using System;
using System.Reflection;

namespace LL.Utilities.Std.Json
{
    public static class JObjectExtension
    {
        private static ProxyGenerator _generator = new ProxyGenerator();

        public static dynamic toProxy(this JObject targetObject, Type interfaceType) 
        {
            return _generator.CreateInterfaceProxyWithoutTarget(interfaceType, new JObjectInterceptor(targetObject));
        }

        public static InterfaceType toProxy<InterfaceType>(this JObject targetObject)
        {

            return toProxy(targetObject, typeof(InterfaceType));
        }
    }

    [Serializable]
    public class JObjectInterceptor : IInterceptor
    {
        private JObject _target;

        public JObjectInterceptor(JObject target)
        {
            _target = target;
        }
        public void Intercept(IInvocation invocation)
        {

            var methodName = invocation.Method.Name;
            if(invocation.Method.IsSpecialName && methodName.StartsWith("get_"))
            {
                var returnType = invocation.Method.ReturnType;
                methodName = methodName.Substring(4);

                if (_target == null || _target[methodName] == null)
                {
                    if (returnType.GetTypeInfo().IsPrimitive || returnType.Equals(typeof(string)))
                    {

                        invocation.ReturnValue = null;
                        return;
                    }

                }

                if (returnType.GetTypeInfo().IsPrimitive || returnType.Equals(typeof(string)))
                {
                    invocation.ReturnValue = _target[methodName].ToObject(returnType);
                }
                else
                {
                    invocation.ReturnValue = ((JObject)_target[methodName]).toProxy(returnType);
                }
            }
            else
            {
                throw new NotImplementedException("Only get accessors are implemented in proxy");
            }

        }
    }



}

Usage:

var jObj = JObject.Parse(input);
InterfaceType proxyObject = jObj.toProxy<InterfaceType>();
Sudsy
la source
Merci! C'est la seule réponse qui prend correctement en charge le typage dynamique (typage canard) sans forcer les restrictions sur le json entrant.
Philip Pittle
Aucun problème. J'ai été un peu surpris de voir qu'il n'y avait rien là-bas. Il a évolué un peu depuis cet exemple original, j'ai donc décidé de partager le code. github.com/sudsy/JsonDuckTyper . Je l'ai également publié sur nuget sous le nom de JsonDuckTyper. Si vous trouvez que vous souhaitez l'améliorer, envoyez-moi simplement un PR et je serai heureux de vous aider.
Sudsy
Lorsque je cherchais une solution dans ce domaine, je suis également tombé sur github.com/ekonbenefits/impromptu-interface . Cela ne fonctionne pas dans mon cas car il ne prend pas en charge dotnet core 1.0, mais cela pourrait fonctionner pour vous.
Sudsy
J'ai essayé avec Impromptu Interface, mais Json.Net n'était pas content de faire un PopulateObjectsur le proxy généré par Impromptu Interface. J'ai malheureusement abandonné Duck Typing - il était simplement plus facile de créer un sérialiseur de contrat Json personnalisé qui utilisait la réflexion pour trouver une implémentation existante de l'interface demandée et l'utiliser.
Philip Pittle
1

Utilisez ce JsonKnownTypes , c'est une manière très similaire d'utiliser, il suffit d'ajouter un discriminateur à json:

[JsonConverter(typeof(JsonKnownTypeConverter<Interface1>))]
[JsonKnownType(typeof(MyClass), "myClass")]
public interface Interface1
{  }
public class MyClass : Interface1
{
    public string Something;
}

Maintenant, lorsque vous sérialisez un objet dans json sera ajouté "$type"avec"myClass" valeur et il sera utilisé pour désérialiser

Json:

{"Something":"something", "$type":"derived"}
Dmitry
la source
0

Ma solution a été ajoutée les éléments d'interface dans le constructeur.

public class Customer: ICustomer{
     public Customer(Details details){
          Details = details;
     }

     [JsonProperty("Details",NullValueHnadling = NullValueHandling.Ignore)]
     public IDetails Details {get; set;}
}
Jorge Santos Neill
la source