Bueno… Honestamente: este post viene a colación de que se estuvo hablando por Linkedin de dedicar hoy (4 de Marzo) un post o algo a WebApi. He de decir que personalmente, no suelo planificar de que escribo. Es decir, tengo varias series abiertas de posts, montones de artículos en borrador y luego voy escribiendo cosas según me van viniendo.
WebApi es un tema que he tratado bastante en mi blog. Uno de los temas que NO he tratado y que era un buen candidato era la exposición de servicios OData usando WebApi. Pero Marc Rubiño se me adelantó y lo contó de manera fenomenal en su blog. Así que pensando temas sobre los que poder hablar al final he optado por este:
Este error se produce si haces algo como p. ej:
public class BeersController : ApiController
{
// GET api/values
public IEnumerable Get()
{
return new ArrayList() {new {Name = "ufo", Value = 10}, new {Age = 20, Name = "Gag"}};
}
}
Es decir si devuelves un objeto anónimo (en mi caso estoy devolviendo una colección de objetos anónimos pero si devuelves uno solo ocurre lo mismo).
La primera opción es pensar que WebApi no soporta objetos anónimos. Pero es falso. WebApi, como tal, no tiene problema alguno en devolver objetos anónimos. Si llamo al mismo servicio pero usando una cabecera Accept application/json para que me devuelva el resultado en json:
En json funciona correctamente. Y es que el problema no está en WebApi en sí… Si no en el serializador de XML que WebApi usa.
Básicamente y resumiendo: El serializador de XML de WebApi no soporta objetos anónimos.
¿Y hay solución para ello?
¡Pues claro! Crearte tu propio serializador de XML y luego un media type formatter asociado que lo use.
Lo primero es eliminar el media type formatter de XML que viene por defecto. Para ello, en Application_Start:
config.Formatters.Remove(config.Formatters.XmlFormatter);
Con esto eliminamos el media type formatter de XML y por lo tanto WebApi deja de dar soporte a XML. Este media type formatter que viene por defecto utiliza o bien DataContractSerializer o bien XmlSerializer para serializar los datos y ninguno de los dos tiene soporte para tipos anónimos. Lo primero es crearnos un serializador de XML propio que tenga soporte para tipos anónimos.
Yo he usado uno (muy sencillo y nada “personalizable”) que he encontrado en http://stackoverflow.com/questions/2404685/can-i-serialize-anonymous-types-as-xml. Simplemente lo he completado para que tenga soporte para colecciones de elementos. El código del serializador es:
public static class CustomXmlSerializer
{
private static readonly Type[] WriteTypes = new[] {
typeof(string), typeof(DateTime), typeof(Enum),
typeof(decimal), typeof(Guid) };
public static bool IsEnumerable(this Type type)
{
return type.GetInterface("IEnumerable") != null;
}
public static bool IsSimpleType(this Type type)
{
return type.IsPrimitive || WriteTypes.Contains(type);
}
public static XElement ToXml(this object input)
{
return input.ToXml(null);
}
public static XElement ToXml(this object input, string element)
{
if (input == null)
return null;
if (string.IsNullOrEmpty(element))
element = "object";
element = XmlConvert.EncodeName(element);
var ret = new XElement(element);
if (input != null)
{
var type = input.GetType();
var props = type.GetProperties();
var elements = from prop in props
let name = XmlConvert.EncodeName(prop.Name)
let val = prop.GetValue(input, null)
let value = prop.PropertyType.IsSimpleType()
? new XElement(name, val)
: val.ToXml(name)
where value != null
select value;
ret.Add(elements);
}
return ret;
}
}
El código es bastante simple no? Básicamente itera sobre todas las propiedades del elemento y usa Linq to XML para convertirlas a XML.
Una vez tenemos un serializador que nos soporta tipos anónimos, ha llegado el momento de crear un media type formatter que lo use. Recuerda que los media type formatters son las clases que usa WebApi para leer/escribir desde/el stream de la petición/respuesta.
En nuestro caso nuestro media type formatter soportará escritura solamente. Eso signfica que podremos devolver tipos anónimos en XML pero NO soportaremos la lectura de XML en las peticiones (es decir no aceptaremos peticiones cuyo cuerpo sea un XML (al menos no con este media type formatter)).
El código es realmente simple:
public class CustomXmlFormatter : BufferedMediaTypeFormatter
{
public CustomXmlFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xml"));
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/xml"));
}
public override bool CanReadType(Type type)
{
return false;
}
public override bool CanWriteType(Type type)
{
return true;
}
public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content)
{
using (var writer = new StreamWriter(writeStream))
{
WriteAny(writer, type, value);
}
}
private void WriteAny(StreamWriter writer, Type type, object value)
{
if (type.IsEnumerable()) WriteCollection(writer, type, value);
else WriteObject(writer, type, value);
}
private void WriteObject(StreamWriter writer, Type type, object value)
{
var xml = value.ToXml();
writer.Write(xml.ToString());
}
private void WriteCollection(StreamWriter writer, Type type, object value)
{
var collection = value as IEnumerable;
writer.Write("<collection>");
foreach (var item in collection)
{
if (item != null)
{
WriteAny(writer, item.GetType(), item);
}
}
writer.Write("</collection>");
}
}
¡Listos! Simplemente redefinimos los métodos CanWriteType para indicar que podemos serializar cualquier tipo y CanReadType para indicar que NO podemos leer ninguno.
Luego redefinimos el método WriteToStream y usamos los métodos extensores definidos en nuestro serializador de xml propio para obtener el XML y escribirlo en el stream de salida.
Ahora tan solo nos queda registrar este media type formatter para que WebApi lo use:
config.Formatters.Add(new CustomXmlFormatter());
Y ahora ya estamos. Si ejecutamos de nuevo la llamada a nuestro servicio con una cabecera Accept que prefiera XML:
Resumen
- WebApi usa el concepto de media type formatter para decidir que clase usar para serializar los datos devueltos por los controladores (también para leer los datos enviados a los controladores)
- Un media type formatter básicamente se asocia a uno o varios tipos mime (eso se hace en el constructor de cada media type formatter)
- El media type formatter por defecto que viene en WebApi para serializar XML usa DataContractSerializer o XmlSerializer y NO tiene soporte para tipos anónimos
- Para habilitar el soporte de tipos anónimos debemos pues crearnos un media type formatter propio que use un serializador de XML que admita tipos anónimos.
- Para JSON no es necesario hacer nada de esto: los tipos anónimos están soportados de serie.
Espero que te haya resultado interesante.
Saludos!
Muy bueno Eduard, sencillo pero muy bueno…