Deserializando Json a tipos derivados en Asp.Net Web API

Estoy llamando a un método de mi WebAPI enviando un JSON que me gustaría asociar (o vincular) con un modelo.

En el controlador tengo un método como:

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

‘MyClass’, que se da como parámetro es una clase abstracta. Me gustaría que, en función del tipo de json aprobado, se crea una instancia de la clase heredada correcta.

Para lograrlo, estoy tratando de implementar una carpeta personalizada. El problema es que (no sé si es muy básico pero no puedo encontrar nada). No sé cómo recuperar el Json en bruto (o mejor, algún tipo de serialización) que viene en la solicitud.

Ya veo:

  • actionContext.Request.Content

Pero todos los métodos están expuestos como asincrónicos. No sé quién encaja con pasar el modelo de generación al método de controlador …

¡Muchas gracias!

No necesita un archivador de modelo personalizado. Tampoco necesita perder el tiempo con la tubería de solicitud.

Eche un vistazo a este otro SO: ¿Cómo implementar JsonConverter personalizado en JSON.NET para deserializar una Lista de objetos de la clase base? .

Usé esto como la base de mi propia solución para el mismo problema.

Comenzando con el JsonCreationConverter referencia en ese SO (ligeramente modificado para corregir problemas con la serialización de tipos en las respuestas):

 public abstract class JsonCreationConverter : JsonConverter { ///  /// this is very important, otherwise serialization breaks! ///  public override bool CanWrite { get { return false; } } ///  /// Create an instance of objectType, based properties in the JSON object ///  /// type of object expected /// contents of JSON object that will be /// deserialized ///  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(); } } 

Y ahora puede anotar su tipo con JsonConverterAttribute , apuntando a Json.Net a un convertidor personalizado:

 [JsonConverter(typeof(MyCustomConverter))] public abstract class BaseClass{ private class MyCustomConverter : JsonCreationConverter { protected override BaseClass Create(Type objectType, Newtonsoft.Json.Linq.JObject jObject) { //TODO: read the raw JSON object through jObject to identify the type //eg here I'm reading a 'typename' property: if("DerivedType".Equals(jObject.Value("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; } } 

Ahora puede usar el tipo de base como parámetro:

 public Result Post(BaseClass arg) { } 

Y si tuviéramos que publicar:

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

Entonces arg sería una instancia de DerivedClass , pero si publicamos:

 { DefaultProperty: 'world' } 

Entonces obtendrías una instancia de DefaultClass .

EDITAR – Por qué prefiero este método a TypeNameHandling.Auto/All

Creo que el uso de TypeNameHandling.Auto/All propugnado por JotaBe no siempre es la solución ideal. Bien podría ser en este caso, pero personalmente no lo haré a menos que:

  • Mi API solo la voy a usar yo o mi equipo
  • No me importa tener un punto final dual compatible con XML

Cuando se utiliza Json.Net TypeNameHandling.Auto o All , su servidor web comenzará a enviar nombres de tipos en el formato MyNamespace.MyType, MyAssemblyName .

He dicho en comentarios que creo que esto es una preocupación de seguridad. Mencioné esto en algunos documentos que leí de Microsoft. Ya no se menciona, parece, sin embargo, todavía siento que es una preocupación válida. No quiero exponer nunca los nombres de tipos y nombres de ensamblaje calificados para el espacio de nombres al mundo exterior. Está aumentando mi superficie de ataque. Entonces, sí, no puedo tener propiedades / parámetros Object mis tipos de API, pero ¿quién puede decir que el rest de mi sitio está completamente libre de agujeros? ¿Quién puede decir que un punto final futuro no expone la capacidad de explotar nombres de tipos? ¿Por qué arriesgarse solo porque es más fácil?

Además, si está escribiendo una API “adecuada”, es decir, específicamente para el consumo de terceros y no solo para usted, y está utilizando la API web, entonces lo más probable es que esté buscando aprovechar el tipo de contenido JSON / XML. manejo (como mínimo). Vea hasta qué punto intenta escribir una documentación que sea fácil de consumir, lo que hace referencia a todos los tipos de API de forma diferente para los formatos XML y JSON.

Al omitir el modo en que JSON.Net comprende los nombres de los tipos, puede alinearlos, haciendo que la elección entre XML / JSON para su interlocutor se base exclusivamente en el gusto, en lugar de que los nombres de tipo sean más fáciles de recordar en uno u otro.

No necesita implementarlo usted mismo. JSON.NET tiene soporte nativo para ello.

Debe especificar la opción deseada TypeNameHandling para el formateador JSON, así (en el evento de inicio de la aplicación global.asax ):

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

Si especifica Auto , como en la muestra anterior, el parámetro se deserializará al tipo especificado en la propiedad $type del objeto. Si falta la propiedad $type , se deserializará al tipo del parámetro. Entonces solo tiene que especificar el tipo cuando está pasando un parámetro de un tipo derivado. (Esta es la opción más flexible).

Por ejemplo, si pasa este parámetro a una acción de API web:

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

El parámetro se deserializará a un objeto de la clase MyNamespace.MyType .

Esto también funciona para sub-propiedades, es decir, puede tener un objeto como este, que especifica que una propiedad interna es de un tipo dado

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

Aquí puede ver un ejemplo en la documentación de JSON.NET de TypeNameHandling.Auto .

Esto funciona al menos desde el lanzamiento de JSON.NET 4 .

NOTA

No necesita decorar nada con attirbutes, ni hacer ninguna otra personalización. Funcionará sin ningún cambio en su código API web.

NOTA IMPORTANTE

El $ tipo debe ser la primera propiedad del objeto serializado JSON . Si no, será ignorado.

COMPARACIÓN CON JSONConverter / JsonConverterAttribute PERSONALIZADO

Estoy comparando la solución nativa con esta respuesta .

Para implementar JsonConverter / JsonConverterAttribute :

  • necesita implementar un JsonConverter personalizado y un JsonConverterAttribute personalizado
  • necesitas usar atributos para marcar los parámetros
  • necesita saber de antemano los posibles tipos esperados para el parámetro
  • necesita implementar o cambiar la implementación de su JsonConverter cada vez que JsonConverter sus tipos o propiedades
  • hay un código que huele a cadenas mágicas , para indicar los nombres de propiedad esperados
  • no estás implementando algo genérico que pueda usarse con cualquier tipo
  • estás reinventando la rueda

En el autor de la respuesta hay un comentario sobre seguridad. A menos que haga algo incorrecto (como aceptar un tipo demasiado genérico para su parámetro, como Object ), no hay riesgo de obtener una instancia del tipo incorrecto: la solución nativa JSON.NET solo instancia un objeto del tipo del parámetro, o un tipo derivado de ella (si no, obtienes null ).

Y estas son las ventajas de la solución nativa JSON.NET:

  • no necesita implementar nada (solo tiene que configurar TypeNameHandling una vez en su aplicación)
  • no necesita usar atributos en sus parámetros de acción
  • no es necesario que conozca los posibles tipos de parámetros de antemano: simplemente necesita saber el tipo base y especificarlo en el parámetro (podría ser un tipo abstracto, para hacer que el polymorphism sea más obvio)
  • la solución funciona para la mayoría de los casos (1) sin cambiar nada
  • esta solución es ampliamente probada y optimizada
  • no necesitas cuerdas mágicas
  • la implementación es genérica y aceptará cualquier tipo derivado

(1): si desea recibir valores de parámetros que no hereden del mismo tipo de base, esto no funcionará, pero no veo sentido al hacerlo

Así que no puedo encontrar ninguna desventaja y encuentro muchas ventajas en la solución JSON.NET.

POR QUÉ USAR JSONConverter / JsonConverterAttribute PERSONALIZADO

Esta es una buena solución de trabajo que permite la personalización, que puede modificarse o ampliarse para adaptarla a su caso particular.

Si desea hacer algo que la solución nativa no puede hacer, como personalizar los nombres de tipo o inferir el tipo de parámetro en función de los nombres de propiedad disponibles, entonces use esta solución adaptada a su propio caso. El otro no puede ser personalizado, y no funcionará para sus necesidades.

Puede llamar a los métodos asíncronos normalmente, su ejecución simplemente se suspenderá hasta que el método regrese y puede devolver el modelo de manera estándar. Simplemente haga una llamada como esta:

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

Le dará JSON sin procesar.

Si quiere usar TypeNameHandling.Auto pero le preocupa la seguridad o no le gusta que los consumidores de API necesiten ese nivel de conocimiento detrás de escena, puede manejar el tipo de $ deserializarse.

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

Luego enganche esto en global.asax.Application__Start

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

finalmente he usado una clase contenedora y [JsonProperty (TypeNameHandling = TypeNameHandling.Auto)] en una propiedad que contiene el objeto con diferentes tipos ya que no he podido hacer que funcione al configurar la clase real.

Este enfoque permite a los consumidores incluir la información necesaria en su solicitud al tiempo que permite que la documentación de los valores permitidos sea independiente de la plataforma, fácil de cambiar y fácil de entender. Todo sin tener que escribir su propio conversor.

Acceda a: https://mallibone.com/post/serialize-object-inheritance-with-json.net para mostrarme el deserializador personalizado de esa propiedad de campo.