Interfaces de Casting para Desserialização no JSON.NET

Eu estou tentando configurar um leitor que terá em objects JSON de vários sites (acho que o scraping de informações) e traduzi-los em objects C #. Atualmente estou usando o JSON.NET para o processo de desserialização. O problema que estou correndo é que ele não sabe como lidar com propriedades de nível de interface em uma class. Então, algo da natureza:

public IThingy Thing 

Produzirá o erro:

Não foi possível criar uma instância do tipo IThingy. Type é uma interface ou class abstrata e não pode ser instanciada.

É relativamente importante que seja um IThingy em oposição a um Thingy, pois o código em que estou trabalhando é considerado sensível e o teste de unidade é altamente importante. Zombar de objects para scripts de testes atômicos não é possível com objects completos como o Thingy. Eles devem ser uma interface.

Eu tenho estudado sobre a documentação do JSON.NET por um tempo agora, e as perguntas que eu encontrei neste site relacionadas a isso são de mais de um ano atrás. Qualquer ajuda?

Além disso, se for importante, meu aplicativo será escrito no .NET 4.0.

   

O @SamualDavis forneceu uma ótima solução em uma questão relacionada , que resumirei aqui.

Se você tiver que desserializar um stream JSON em uma class concreta que tenha propriedades de interface, inclua as classs concretas como parâmetros em um construtor para a class! O deserializador NewtonSoft é inteligente o suficiente para descobrir que ele precisa usar essas classs concretas para desserializar as propriedades.

Aqui está um exemplo:

 public class Visit : IVisit { ///  /// This constructor is required for the JSON deserializer to be able /// to identify concrete classs to use when deserializing the interface properties. ///  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; } } 

(Copiado desta pergunta )

Nos casos em que não tive controle sobre o JSON de input (e, portanto, não posso garantir que ele inclua uma propriedade $ type), escrevi um conversor personalizado que permite especificar explicitamente o tipo concreto:

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

Isso apenas usa a implementação do serializador padrão do Json.Net, especificando explicitamente o tipo concreto.

Uma visão geral está disponível nesta postagem do blog . O código fonte está abaixo:

 public class ConcreteTypeConverter : 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(reader); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { //use the default serialization - it works fine serializer.Serialize(writer, value); } } 

Para habilitar a desserialização de várias implementações de interfaces, você pode usar o JsonConverter, mas não por meio de um atributo:

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

O DTOJsonConverter mapeia cada interface com uma implementação concreta:

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

O DTOJsonConverter é necessário apenas para o desserializador. O processo de serialização é inalterado. O object Json não precisa incorporar nomes de tipos concretos.

Esta postagem SO oferece a mesma solução um passo adiante com um JsonConverter genérico.

Por que usar um conversor? Existe uma funcionalidade nativa no Newtonsoft.Json para resolver este problema exato:

Definir TypeNameHandling no JsonSerializerSettings para TypeNameHandling.Auto

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

Isso colocará cada tipo no json, que não é mantido como uma instância concreta de um tipo, mas como uma interface ou uma class abstrata.

Eu testei, e funciona como um encanto, mesmo com listas.

Fonte e uma implementação manual alternativa: Code Inside Blog

Eu achei isso útil. Você também pode.

Exemplo de uso

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

Conversor de criação personalizada

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

Documentação do Json.NET

Duas coisas que você pode tentar:

Implemente um modelo 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(reader); if (richDude == null) { richDude = serializer.Deserialize(reader); } if (richDude == null) { richDude = serializer.Deserialize(reader); } return richDude; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { // Left as an exercise to the reader :) throw new NotImplementedException(); } } 

Ou, se você puder fazer isso em seu modelo de object, implemente uma class base concreta entre o IPerson e seus objects folha e desserialize para ele.

O primeiro pode falhar potencialmente em tempo de execução, o segundo requer alterações em seu modelo de object e homogeneiza a saída para o menor denominador comum.

Para aqueles que podem estar curiosos sobre o ConcreteListTypeConverter que foi referenciado por Oliver, aqui está minha tentativa:

 public class ConcreteListTypeConverter : 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>(reader); return res.ConvertAll(x => (TInterface) x); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { serializer.Serialize(writer, value); } } 

Suponha que uma configuração de autofac seja semelhante à seguinte:

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

Então, suponha que sua aula seja assim:

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

Portanto, o uso do resolvedor na desserialização poderia ser como:

 ContainerBuilder builder = new ContainerBuilder(); builder.RegisterType().As(); builder.RegisterType(); builder.Register(c => new LogService(new DateTime(2000, 12, 12))).As(); 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(json, new JsonSerializerSettings { ContractResolver = contractResolver }); Console.WriteLine(controller.Repository.GetType().Name); 

Você pode ver mais detalhes em http://www.newtonsoft.com/json/help/html/DeserializeWithDependencyInjection.htm

Use esta class, para mapear o tipo abstrato para o tipo real:

 public class AbstractConverter : JsonConverter { public override Boolean CanConvert(Type objectType) => objectType == typeof(TAbstract); public override Object ReadJson(JsonReader reader, Type type, Object value, JsonSerializer jser) => jser.Deserialize(reader); public override void WriteJson(JsonWriter writer, Object value, JsonSerializer jser) => jser.Serialize(writer, value); } 

… e quando desserializar:

  var settings = new JsonSerializerSettings { Converters = { new AbstractConverter(), new AbstractConverter() }, }; JsonConvert.DeserializeObject(json, type, settings); 

Por que vale a pena, acabei tendo que lidar com isso sozinho na maior parte do tempo. Cada object tem um método Deserialize (string jsonStream) . Alguns trechos disso:

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

Nesse caso, o novo Thingy (string) é um construtor que chamará o método Deserialize (string jsonStream) do tipo concreto apropriado. Este esquema continuará indo para baixo e para baixo até você chegar aos pontos de base que o json.NET pode manipular.

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

Então, e assim por diante. Essa configuração me permitiu dar configurações json.NET que ele pode manipular sem ter que refatorar uma grande parte da biblioteca em si ou usar modelos try / parse que dificultariam nossa biblioteca inteira devido ao número de objects envolvidos. Isso também significa que eu posso efetivamente lidar com qualquer mudança de json em um object específico, e não preciso me preocupar com tudo que esse object toca. Não é de forma alguma a solução ideal, mas funciona muito bem com nossos testes de unidade e integração.

Vários anos depois e eu tive um problema semelhante. No meu caso, havia interfaces fortemente aninhadas e uma preferência para gerar as classs concretas em tempo de execução, de modo que funcionasse com uma class genérica.

Eu decidi criar uma class de proxy em tempo de execução que envolve o object retornado pela Newtonsoft.

A vantagem dessa abordagem é que ela não exige uma implementação concreta da class e pode manipular qualquer profundidade de interfaces aninhadas automaticamente. Você pode ver mais sobre isso no meu 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(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"); } } } } 

Uso:

 var jObj = JObject.Parse(input); InterfaceType proxyObject = jObj.toProxy(); 

Nicholas Westby forneceu uma ótima solução em um artigo incrível .

Se você quiser desserializar o JSON para uma das muitas classs possíveis que implementam uma interface como essa:

 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#" } }; } } 

Você pode usar um conversor JSON personalizado:

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

E você precisará decorar a propriedade “Profession” com um atributo JsonConverter para que ele saiba usar seu conversor personalizado:

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

E então, você pode transmitir sua turma com uma interface:

 Person person = JsonConvert.DeserializeObject(jsonString); 

Minha solução para este, que eu gosto porque é bem geral, é a seguinte:

 ///  /// Automagically convert known interfaces to (specific) concrete classs on deserialisation ///  public class WithMocksJsonConverter : JsonConverter { ///  /// The interfaces I know how to instantiate mapped to the classs with which I shall instantiate them, as a Dictionary. ///  private readonly Dictionary conversions = new Dictionary() { { typeof(IOne), typeof(MockOne) }, { typeof(ITwo), typeof(MockTwo) }, { typeof(IThree), typeof(MockThree) }, { typeof(IFour), typeof(MockFour) } }; ///  /// Can I convert an object of this type? ///  /// The type under consideration /// True if I can convert the type under consideration, else false. public override bool CanConvert(Type objectType) { return conversions.Keys.Contains(objectType); } ///  /// Attempt to read an object of the specified type from this reader. ///  /// The reader from which I read. /// The type of object I'm trying to read, anticipated to be one I can convert. /// The existing value of the object being read. /// The serializer invoking this request. /// An object of the type into which I convert the specified objectType. 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)); } } ///  /// Not yet implemented. ///  /// The writer to which I would write. /// The value I am attempting to write. /// the serializer invoking this request. public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); } } 

}

Você poderia obviamente e trivialmente convertê-lo em um conversor ainda mais geral, adicionando um construtor que tomou um argumento do tipo Dictionary com o qual instanciar a variável de instância de conversões.

Nenhum object será um IThingy, pois as interfaces são todas abstratas por definição.

O object que você primeiro serializou foi de algum tipo concreto , implementando a interface abstrata . Você precisa ter essa mesma class concreta para reviver os dados serializados.

O object resultante será de algum tipo que implementa a interface abstrata que você está procurando.

Da documentação segue que você pode usar

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

ao desserializar para informar o JSON.NET sobre o tipo concreto.

Minha solução foi adicionada aos elementos da interface no construtor.

 public class Customer: ICustomer{ public Customer(Details details){ Details = details; } [JsonProperty("Details",NullValueHnadling = NullValueHandling.Ignore)] public IDetails Details {get; set;} }