WebApi–Recibir en un controlador un IEnumerable desde URL

Muy buenas!

En un proyecto en el que estoy trabajando ha surgido la necesidad de pasarle via GET una lista de ids con los que hacer algo. La acción del controlador FinishersController está declarada de la siguiente manera:

public IEnumerable<TrackingAndCompetitorDTO> GetByRaces(IEnumerable<int> id)

{

    return null;

}

Ahora viene el momento de llamar al controlador:

  • /api/Finishers/10,20,30 –> Devuelve un 404
  • /api/Finishers/10 –> Enlaza a al acción pero id es null
  • /api/Finishers/?id=10,20,30 –> Enlaza a la acción pero id es null
  • /api/Finishers/?id=10&id=20&id=30 –> Enlaza a la acción pero id es null
  • /api/Finishers/?id[0]=10&id[1]=20&id[2]=30 –> Enlaza a la acción pero id es null

Si tienes experiencia en ASP.NET MVC entenderás que las tres primeras fallen es comprensible (también fallarían en MVC). Pero las dos últimas en MVC funcionarían correctamente… ¿entonces por qué fallan en WebApi?

La forma en como se realiza el enlace de parámetros de la request hacia los controladores es totalmente diferente en WebApi que en MVC. Insisto: WebApi es otra cosa completamente distinta aunque luzca muy parecida a ASP.NET MVC y aunque vengan juntos. No des por supuesto nada de lo que sabes en ASP.NET MVC para WebApi. Algunas cosas funcionan igual, otras, completamente distinto.

En ASP.NET MVC el enlace desde la request hacia los controladores se realiza mediante los ValueProviders y los Model Binders. Los primeros son los encargados de inspeccionar la request (formdata, querystring, pathinfo, headers, …) y dejar los valores en un sitio común. Luego los model binders leen de “ese sitio común” y construyen los valores de los parámetros que el controlador recibe.

WebApi añade a estos dos conceptos, un tercero: los media type formatters. Bien, recuerda siempre la gran diferencia entre WebApi y MVC: En MVC se usa un buffer para guardar la petición (y la respuesta). Eso significa que los value providers pueden leer n veces el cuerpo de la petición sin que de error. En WebApi NO. WebApi es, por decirlo de algún modo, stream-based. Nuestros media type formatters reciben un stream y pueden leer de él una sola vez. Por lo tanto tan solo un media type formatter puede leer el cuerpo de la petición.

Pero ojo… he dicho que WebApi añade el concepto de media type formatters, porque en WebApi también se usan value providers y model binders. ¿Cuando? Pues para enlazar parámetros que no provienen del cuerpo de la petición (o sea, usualmente de la URL). Esa es la norma báscia:

  1. El parámetro no está en el cuerpo? Se enlaza vía un model binder
  2. El parámetro está en el cuerpo de la petición? Se enlaza via un media type formatter

Webapi hace ciertas asunciones sobre si un parámetro debe enlazarse via model binder o media type formatter. Básicamente: si es tipo simple se usará un model binder. Si es un tipo complejo se usará un media type formatter. Por lo tanto, por defecto nuestro parámetro IEnumerable<int> al NO ser un tipo simple se intenta enlazar mediante un media type formatter. De ahi que no se encuentren datos porque los media type formatters miran tan solo el cuerpo y en nuestro caso está vacío.

¿Y como podemos modificar este comportamiento? Pues bien:

  1. Si decoras un parámetro con [FromUri] indicas a WebApi que este parámetro vendrá en la URL
  2. Si decoras un parámetro con [FromBody] indicas a WebApi que este parámetro vendrá en el cuerpo de la petición
  3. Si decoras un parámetro con [ModelBinder] puedes especificar un Model Binder específico para tu parámetro. (De hecho [FromUri] deriva de [ModelBinder]).
  4. Y recuerda: El cuerpo de la petición tan solo puede leerse una vez. Por lo tanto si tu controlador recibe dos parámetros complejos tan solo uno puede ser procesado mediante un media type formatter (y venir en el cuerpo). El otro debe ser procesado mediante un model binder y venir en alguna otra parte que NO sea el cuerpo de la petición  (y estar decorado con [ModelBinder] o [FromUri]).

Bueno… ahora ya ves la solución a nuestro problema no? Basta con decorar el parámetro con [FromUri]:

public IEnumerable<TrackingAndCompetitorDTO> GetByRaces([FromUri] IEnumerable<int> id)

{

    return null;

}

Con esto estamos forzando a WebApi a que enlace este parámetro usando los value providers y model binders. Y ahora la URL

  • /api/Finishers/?id=10&id=20&id=30 –> Funciona correctamente.

Curiosamente la URL

  • /api/Finishers?id[0]=10&id[1]=20&id[2]=30 NO funciona, pero eso ya se debe a diferencias de como está implementado el Model binder de WebApi y el de MVC cuando tratan con colecciones…

Saludos!

WebApi–Devolver tipos anónimos en XML

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:

image

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:

image

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:

image

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!