c# - Casting interfaces para deserialización en JSON.NET
.net-4.0 (15)
Estoy intentando configurar un lector que tome los objetos JSON de varios sitios web (piense en raspado de información) y los traduzca a objetos C #. Actualmente estoy usando JSON.NET para el proceso de deserialización. El problema al que me estoy enfrentando es que no sabe cómo manejar las propiedades de nivel de interfaz en una clase. Entonces algo de la naturaleza:
public IThingy Thing
Producirá el error:
No se pudo crear una instancia de tipo IThingy. Tipo es una interfaz o clase abstracta y no se puede crear una instancia.
Es relativamente importante tenerlo como IThingy en lugar de Thingy, ya que el código en el que estoy trabajando se considera confidencial y las pruebas unitarias son muy importantes. La burla de los objetos para los scripts de pruebas atómicas no es posible con objetos completamente desarrollados como Thingy. Deben ser una interfaz.
He estado estudiando detenidamente la documentación de JSON.NET desde hace un tiempo, y las preguntas que pude encontrar en este sitio relacionadas con esto son todas de hace más de un año. ¿Alguna ayuda?
Además, si es importante, mi aplicación está escrita en .NET 4.0.
¿Por qué usar un convertidor? Hay una funcionalidad nativa en Newtonsoft.Json
para resolver este problema exacto:
Establezca TypeNameHandling
en JsonSerializerSettings
en TypeNameHandling.Auto
JsonConvert.SerializeObject(
toSerialize,
new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.Auto
});
Esto pondrá cada tipo en el JSON, que no se mantiene como una instancia concreta de un tipo, sino como una interfaz o una clase abstracta.
Lo probé, y funciona como un encanto, incluso con listas.
Fuente y una implementación manual alternativa: Code Inside Blog
(Copiado de esta pregunta )
En los casos en los que no he tenido control sobre el JSON entrante (y por lo tanto no puedo asegurar que incluya una propiedad $ type) he escrito un convertidor personalizado que solo le permite especificar explícitamente el tipo concreto:
public class Model
{
[JsonConverter(typeof(ConcreteTypeConverter<Something>))]
public ISomething TheThing { get; set; }
}
Esto solo usa la implementación del serializador predeterminado de Json.Net mientras se especifica explícitamente el tipo de concreto.
Una descripción general está disponible en esta publicación de blog . El código fuente está abajo:
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);
}
}
@SamualDavis brindó una excelente solución en una pregunta relacionada , que resumiré aquí.
Si tiene que deserializar un flujo JSON en una clase concreta que tenga propiedades de interfaz, puede incluir las clases concretas como parámetros para un constructor de la clase. El deserializador de NewtonSoft es lo suficientemente inteligente como para darse cuenta de que necesita utilizar esas clases concretas para deserializar las propiedades.
Aquí hay un ejemplo:
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; }
}
Dos cosas que puedes probar:
Implementar un modelo de prueba / análisis:
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();
}
}
O bien, si puede hacerlo en su modelo de objetos, implemente una clase base concreta entre IPerson y sus objetos de hoja, y deserialícese a ella.
El primero puede fallar en tiempo de ejecución, el segundo requiere cambios en su modelo de objetos y homogeneiza el resultado al mínimo común denominador.
Encontré esto útil. Tú también podrías.
Ejemplo de uso
public class Parent
{
[JsonConverter(typeof(InterfaceConverter<IChildModel, ChildModel>))]
IChildModel Child { get; set; }
}
Convertidor de creación personalizada
public class InterfaceConverter<TInterface, TConcrete> : CustomCreationConverter<TInterface>
where TConcrete : TInterface, new()
{
public override TInterface Create(Type objectType)
{
return new TConcrete();
}
}
Mi solución a esta, que me gusta porque es muy general, es la siguiente:
/// <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();
}
}
}
Obviamente, podría convertirlo en un convertidor aún más general agregando un constructor que tomara un argumento de tipo Diccionario <Tipo, Tipo> con el cual instanciar la variable de instancia de conversiones.
Mi solución fue agregada los elementos de la interfaz en el constructor.
public class Customer: ICustomer{
public Customer(Details details){
Details = details;
}
[JsonProperty("Details",NullValueHnadling = NullValueHandling.Ignore)]
public IDetails Details {get; set;}
}
Nicholas Westby proporcionó una gran solución en un artículo increíble .
Si desea Deserializar JSON a una de las muchas clases posibles que implementan una interfaz como esa:
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#"
}
};
}
}
Puede usar un convertidor 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;
}
}
Y deberá decorar la propiedad "Profession" con un atributo JsonConverter para que sepa utilizar su convertidor personalizado:
public class Person
{
[JsonConverter(typeof(ProfessionConverter))]
public IProfession Profession { get; set; }
}
Y luego, puedes lanzar tu clase con una interfaz:
Person person = JsonConvert.DeserializeObject<Person>(jsonString);
Ningún objeto será nunca IThingy ya que las interfaces son todas abstractas por definición.
El objeto que tienes que se serializó por primera vez fue de algún tipo concreto , implementando la interfaz abstracta . Necesita tener esta misma clase concreta para revivir los datos serializados.
El objeto resultante será de algún tipo que implemente la interfaz abstracta que está buscando.
De la documentation que sigue, puedes usar
(Thingy)JsonConvert.DeserializeObject(jsonString, typeof(Thingy));
al deserializar para informar a JSON.NET sobre el tipo concreto.
Para aquellos que puedan tener curiosidad sobre el ConcreteListTypeConverter al que se refirió Oliver, este es mi intento:
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);
}
}
Para habilitar la deserialización de múltiples implementaciones de interfaces, puede usar JsonConverter, pero no a través de un atributo:
Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer();
serializer.Converters.Add(new DTOJsonConverter());
Interfaces.IEntity entity = serializer.Deserialize(jsonReader);
DTOJsonConverter mapea cada interfaz con una implementación 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);
}
}
DTOJsonConverter solo se requiere para el deserializador. El proceso de serialización no se modifica. El objeto Json no necesita incrustar nombres de tipos concretos.
Esta publicación SO ofrece la misma solución un paso más allá con un JsonConverter genérico.
Por lo que vale, terminé teniendo que manejar esto yo mismo en su mayor parte. Cada objeto tiene un método Deserialize (string jsonStream) . Algunos fragmentos de esto:
JObject parsedJson = this.ParseJson(jsonStream);
object thingyObjectJson = (object)parsedJson["thing"];
this.Thing = new Thingy(Convert.ToString(thingyObjectJson));
En este caso, el nuevo Thingy (cadena) es un constructor que llamará al método Deserialize (string jsonStream) del tipo concreto apropiado. Este esquema continuará yendo hacia abajo y hacia abajo hasta llegar a los puntos base que json.NET puede manejar.
this.Name = (string)parsedJson["name"];
this.CreatedTime = DateTime.Parse((string)parsedJson["created_time"]);
Y así sucesivamente. Esta configuración me permitió ofrecer las configuraciones de json.NET que puede manejar sin tener que refactorizar una gran parte de la biblioteca en sí o utilizar modelos difíciles de probar / analizar que habrían empantanado toda nuestra biblioteca debido a la cantidad de objetos involucrados. También significa que puedo manejar con eficacia cualquier cambio json en un objeto específico, y no necesito preocuparme por todo lo que ese objeto toca. De ninguna manera es la solución ideal, pero funciona bastante bien desde nuestra unidad y las pruebas de integración.
Supongamos que una configuración de autofac como la siguiente:
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;
}
}
Entonces, supongamos que tu clase es así:
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; }
}
}
Por lo tanto, el uso del resolver en deserialización podría ser como:
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);
Puede ver más detalles en http://www.newtonsoft.com/json/help/html/DeserializeWithDependencyInjection.htm
Utilice esta clase para mapear tipo abstracto a tipo real:
public class AbstractConverter<TReal, TAbstract> : JsonConverter
{
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);
}
... y cuando deserialize:
var settings = new JsonSerializerSettings
{
Converters = {
new AbstractConverter<Thing, IThingy>(),
new AbstractConverter<Thing2, IThingy2>()
},
};
JsonConvert.DeserializeObject(json, type, settings);
Varios años después y tuve un problema similar. En mi caso, había interfaces fuertemente anidadas y una preferencia por generar las clases concretas en tiempo de ejecución para que funcionara con una clase genérica.
Decidí crear una clase de proxy en tiempo de ejecución que envuelve el objeto devuelto por Newtonsoft.
La ventaja de este enfoque es que no requiere una implementación concreta de la clase y puede manejar cualquier profundidad de las interfaces anidadas de forma automática. Puedes ver más sobre esto en mi 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");
}
}
}
}
Uso:
var jObj = JObject.Parse(input);
InterfaceType proxyObject = jObj.toProxy<InterfaceType>();