J'essaie de mettre en place un lecteur qui prendra en charge les objets JSON de différents sites Web (pensez à la suppression 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 de niveau 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 classe d'interface ou abstraite et ne peut pas être instancié.
Il est relativement important que ce soit un IThingy par opposition à un Thingy puisque le code sur lequel je travaille est considéré comme sensible et que les tests unitaires sont très importants. Le moquage d'objets pour des scripts de test atomiques n'est pas possible avec des objets à part entière tels que Thingy. Ils doivent être une interface.
Je me penche sur la documentation de JSON.NET depuis un moment maintenant, et les questions que je pourrais trouver sur ce site à ce sujet ont toutes été posées il y a plus d'un an. De l'aide?
De plus, si cela compte, mon application est écrite en .NET 4.0.
@SamualDavis a fourni une excellente solution à une question connexe , que je vais résumer ici.
Si vous devez désérialiser un flux JSON dans une classe concrète possédant 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; }
}
(Copié de cette question )
Dans les cas où je n'ai pas eu le contrôle sur le JSON entrant (et ne peux donc pas garantir 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; }
}
Ceci utilise simplement l'implémentation de sérialiseur par défaut de Json.Net tout en spécifiant explicitement le type concret.
Un aperçu est disponible sur ce 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);
}
}
Pour permettre 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 est requis uniquement 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 SO post offre la même solution un peu plus loin avec un JsonConverter générique.
Pourquoi utiliser un convertisseur? Il existe une fonctionnalité native dans Newtonsoft.Json
pour résoudre ce problème exact:
Définissez TypeNameHandling
dans JsonSerializerSettings
à TypeNameHandling.Auto
JsonConvert.SerializeObject(
toSerialize,
new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.Auto
});
Cela mettra chaque type dans le JSON, qui n'est pas tenu comme une instance concrète d'un type mais comme une interface ou une classe abstraite.
Je l'ai testé, et cela fonctionne comme un charme, même avec des listes.
Source et une implémentation manuelle alternative: Code Inside Blog
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();
}
}
Utilisez cette classe pour mapper un type abstrait sur un 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 quand désérialiser:
var settings = new JsonSerializerSettings
{
Converters = {
new AbstractConverter<Thing, IThingy>(),
new AbstractConverter<Thing2, IThingy2>()
},
};
JsonConvert.DeserializeObject(json, type, settings);
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 d'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.
Supposons un réglage automatique comme suit:
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 soit comme ça:
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 ressembler à:
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 dans http://www.newtonsoft.com/json/help/html/DeserializeWithDependencyInjection.htm
Pour ceux qui pourraient être curieux au sujet du ConcreteListTypeConverter auquel Oliver a fait référence, 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);
}
}
Nicholas Westby a fourni une excellente solution dans un article génial .
Si vous souhaitez désérialiser JSON dans 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 aurez besoin de décorer la propriété "Profession" avec un attribut JsonConverter pour lui faire savoir comment utiliser votre convertisseur personnalisé:
public class Person
{
[JsonConverter(typeof(ProfessionConverter))]
public IProfession Profession { get; set; }
}
Et ensuite, vous pouvez lancer votre classe avec une interface:
Person person = JsonConvert.DeserializeObject<Person>(jsonString);
Pour ce que ça vaut, j'ai fini par devoir gérer moi-même la plupart du temps. 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 de haut en bas jusqu'à atteindre les points de base que json.NET peut gérer.
this.Name = (string)parsedJson["name"];
this.CreatedTime = DateTime.Parse((string)parsedJson["created_time"]);
Etc., etc. Cette configuration m'a permis de donner à json.NET les configurations qu'il peut gérer sans avoir à refactoriser une grande partie de la bibliothèque elle-même ni à utiliser des modèles fastidieux d'essais/analyses qui auraient embourbé notre bibliothèque entière à cause du nombre d'objets impliqués. Cela signifie également que je peux gérer efficacement tous les changements JSON sur un objet spécifique, et je n'ai pas besoin de m'inquiéter de tout ce que l'objet touche. Ce n'est en aucun cas la solution idéale, mais cela fonctionne assez bien à partir de nos tests unitaires et d'intégration.
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 lors 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 enveloppe l'objet renvoyé 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>();
Aucun objet ne sera ever _ be un IThingy car les interfaces sont toutes 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 résumé. Vous devez avoir cette même classe concrète revitaliser les données sérialisées.
L'objet résultant sera alors d'un type que implémente l'interface abstraite que vous recherchez.
Dans la documentation il s’ensuit que vous pouvez utiliser
(Thingy)JsonConvert.DeserializeObject(jsonString, typeof(Thingy));
lors de la désérialisation, informer JSON.NET du type concret.
Ma solution à celle-ci, qui me plait car elle est joliment 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 Dictionnaire <Type, Type> avec lequel instancier la variable d'instance de conversion.
Ma solution a été ajoutée aux éléments d'interface du constructeur.
public class Customer: ICustomer{
public Customer(Details details){
Details = details;
}
[JsonProperty("Details",NullValueHnadling = NullValueHandling.Ignore)]
public IDetails Details {get; set;}
}