WebApi 2–Leer datos desde los headers

En un curso que he impartido sobre WebApi 2 me han comentado un escenario en el que mandaban un conjunto de datos en varias cabeceras HTTP propias y querían leer esos datos desde los controladores.

La verdad es que hay varias maneras de hacer eso en WebApi 2 y vamos a analizar algunas de ellas en este post. Eso nos servirá como excusa para recorrer algunos de los mecanismos de extensibilidad del framework.

Para los ejemplos vamos a suponer que se envían dos cabeceras X-lang y X-version que son las que deseamos leer.

Antes de nada, vamos a crear un poco de infraestructura que usaremos en todas las aproximaciones. Primero la clase “CustomHeaders” que contendrá los valores de los headers:

  1. public class CustomHeaders
  2. {
  3.     public string Lang { get; set; }
  4.     public int Version { get; set; }
  5.     public CustomHeaders()
  6.     {
  7.         Version = -1;
  8.     }
  9. }

Y luego un par de métodos de extensión sobre HttpRequestMessage para leer los headers y para obtener una propiedad de la petición (donde alguien debe haber guardado un objeto CustomHeaders):

  1. static class HttpRequestMessageExtensions
  2. {
  3.     public const string CustomHeadersKey = nameof(CustomHeaders);
  4.     public static CustomHeaders ReadCustomHeaders(this HttpRequestMessage request)
  5.     {
  6.         var customHeaders = new CustomHeaders();
  7.         var headers = request.Headers;
  8.         if (headers.Contains(«X-lang»))
  9.         {
  10.             customHeaders.Lang = headers.GetValues(«X-lang»).FirstOrDefault();
  11.         }
  12.         if (headers.Contains(«X-version»))
  13.         {
  14.             var headerVersion = headers.GetValues(«X-version»).FirstOrDefault();
  15.             if (!string.IsNullOrEmpty(headerVersion))
  16.             {
  17.                 int version;
  18.                 if (int.TryParse(headerVersion, out version))
  19.                 {
  20.                     customHeaders.Version = version;
  21.                 }
  22.             }
  23.         }
  24.         return customHeaders;
  25.     }
  26.     public static void StoreCustomHeaders(this HttpRequestMessage request, CustomHeaders customHeaders)
  27.     {
  28.         if (request.Properties.ContainsKey(CustomHeadersKey))
  29.         {
  30.             request.Properties[CustomHeadersKey] = customHeaders;
  31.         }
  32.         else
  33.         {
  34.             request.Properties.Add(CustomHeadersKey, customHeaders);
  35.         }
  36.     }
  37. }

Ahora ya podemos empezar a ver opciones para leer esos headers y usarlos en los controladores.

1. MessageHandler que lea los headers y deje los datos en el contexto de la petición

En esta opción un MessageHandler procesa todas las peticiones a WebApi, lee las cabeceras y coloca sus datos en el contexto de la petición de WebApi:

  1. public class HeaderMessageHandler : DelegatingHandler
  2. {
  3.     protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
  4.     {
  5.         var customHeaders = request.ReadCustomHeaders();
  6.         request.StoreCustomHeaders(customHeaders);
  7.         return base.SendAsync(request, cancellationToken);
  8.     }
  9. }

Luego necesitamos leer en los controladores el valor de los headers desde el contexto de la petición, donde los ha guardado el MessageHandler:

  1. public class DemoController : ApiController
  2. {
  3.     [Route(«api/demo»)]
  4.     [HttpGet]
  5.     [ResponseType(typeof(CustomHeaders))]
  6.     public IHttpActionResult GetAll()
  7.     {
  8.         return Ok(Request.Properties[HttpRequestMessageExtensions.CustomHeadersKey] as CustomHeaders);
  9.     }
  10. }

Por supuesto, si ves que es muy pesado hacer esto en cada acción siempre puedes derivar de un controlador base que haga eso de forma automática:

  1. public class BaseController : ApiController
  2. {
  3.     protected CustomHeaders CustomHeaders {get; private set;}
  4.     protected override void Initialize(HttpControllerContext controllerContext)
  5.     {
  6.         CustomHeaders = controllerContext.Request.Properties[HttpRequestMessageExtensions.CustomHeadersKey] as CustomHeaders;
  7.         base.Initialize(controllerContext);
  8.     }
  9. }

Así nuestros controladores tienen siempre la propiedad “CustomHeaders” lista para ser leída:

  1. public class DemoController : BaseController
  2. {
  3.     [Route(«api/demo»)]
  4.     [HttpGet]
  5.     [ResponseType(typeof(CustomHeaders))]
  6.     public IHttpActionResult GetAll()
  7.     {
  8.         return Ok(CustomHeaders);
  9.     }
  10. }

Por supuesto, incluso podríamos prescindir del MessageHandler y leer los headers en el Initialize de nuestro controlador. Ten presente que el MessageHanlder actúa para cualquier petición a WebApi, incluso aquellas que no terminan en un controlador (porque no se enrutan correctamente). Por su parte colocarlo en el controlador tan solo lee los headers si la llamada llega a un controlador. Por lo tanto, si en lugar de leer headers debes hacer algo más costoso, eso debes tenerlo en consideración.

2 – Filtros de Acción

Un filtro de acción es otra posibilidad que tienes para procesar esos datos antes de que se invoque la acción:

  1. public class ReadCustomHeadersAttribute : ActionFilterAttribute
  2. {
  3.     public override Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
  4.     {
  5.         var request = actionContext.Request;
  6.         var customHeaders = request.ReadCustomHeaders();
  7.         request.StoreCustomHeaders(customHeaders);
  8.         return base.OnActionExecutingAsync(actionContext, cancellationToken);
  9.     }
  10. }

Luego basta con aplicar el fitro a cualquier acción:

  1. public class DemoController : ApiController
  2. {
  3.     [Route(«api/demo»)]
  4.     [HttpGet]
  5.     [ReadCustomHeaders]
  6.     [ResponseType(typeof(CustomHeaders))]
  7.     public IHttpActionResult GetAll()
  8.     {
  9.         return Ok(Request.Properties[HttpRequestMessageExtensions.CustomHeadersKey] as CustomHeaders);
  10.     }
  11. }

Este mecanismo es mejor si solo hay algunas acciones que requieran leer los headers especiales (o hacer la acción correspondiente). Además queda explícito viendo el código qué acciones dependen de estos headers especiales (las decoradas con el atributo). Por supuesto podríamos añadir este filtro en los filtros globales de WebApi y entonces todas las acciones leerían los headers especiales.

3 – Value provider

Aquí usamos otro enfoque. Hasta ahora leíamos los headers y metíamos un objeto CustomHeaders en el contexto de la petición WebApi. Ahora vamos a añadir un parámetro de tipo “CustomHeaders” a las acciones que lo requieran y dejaremos que sea el proceso de model binding que se encargue de leerlo. Para ello debemos crear un ValueProvider para leer de los headers:

  1. public class HeaderValueProviderFactory : ValueProviderFactory
  2. {
  3.     public override IValueProvider GetValueProvider(HttpActionContext actionContext)
  4.     {
  5.         return new HeaderValueProvider(actionContext.Request);
  6.     }
  7. }
  8. public class HeaderValueProvider : IValueProvider
  9. {
  10.     private readonly HttpRequestMessage _request;
  11.     private CustomHeaders _customHeaders;
  12.     public HeaderValueProvider(HttpRequestMessage request)
  13.     {
  14.         _customHeaders = request.ReadCustomHeaders();
  15.     }
  16.     public bool ContainsPrefix(string prefix)
  17.     {
  18.         return true;
  19.     }
  20.     public ValueProviderResult GetValue(string key)
  21.     {
  22.         if (key == nameof(CustomHeaders.Lang) || key.EndsWith(«.» + nameof(CustomHeaders.Lang)))
  23.         {
  24.             return new ValueProviderResult(_customHeaders.Lang, _customHeaders.Lang.ToString(), CultureInfo.InvariantCulture);
  25.         }
  26.         if (key == nameof(CustomHeaders.Version) || key.EndsWith(«.» + nameof(CustomHeaders.Version)))
  27.         {
  28.             return new ValueProviderResult(_customHeaders.Version, _customHeaders.Version.ToString(), CultureInfo.InvariantCulture);
  29.         }
  30.         return null;
  31.     }
  32. }

Ahora debemos añadir el parámetro de tipo “CustomHeaders” en las acciones que lo necesiten:

  1. public class DemoController : ApiController
  2. {
  3.     [Route(«api/demo»)]
  4.     [HttpGet]
  5.     [HttpPost]
  6.     [ResponseType(typeof(CustomHeaders))]
  7.     public IHttpActionResult GetAll([ValueProvider(typeof(HeaderValueProviderFactory))]CustomHeaders ch)
  8.     {
  9.         return Ok(ch);
  10.     }

Observa que la clave es que el parámetro está decorado con el atributo ValueProvider que le indica el Value Provider a utilizar. Así el model binder de WebApi sabe que debe usar este Value Provider para este parámetro.

Si quieres ver una implementación genérica de un ValueProvider para enlazar parámetros desde los headers mira este enlace.

4 – Usar un Model Binder

Esta opción es conceptualmente errónea, pero vamos a discutirla de todos modos. Digo que es conceptualmente errónea porque un model binder debería utilizarse cuando es el “formato de los datos” y no su “úbicación dentro de la petición” lo que cambia respecto a lo esperado por Web Api. P. ej. usaríamos un model binder para enlazar una query string de tipo ?fibo=1,1,2,3,5,8 a un IEnumerable<int>, porque WebApi no sabe por defecto enlazar IEnumerable<int> si vienen como una lista de enteros separados por comas. En este caso, como el formato de los datos (enteros separados por comas) es distinto de lo que espera Web Api es correcto usar un model binder. En nuestro caso lo suyo es usar un Value Provider como ya se ha visto en el punto anterior.

Aclarado esto, veamos como sería usando un model binder:

  1. public class CustomHeadersModelBinder : IModelBinder
  2. {
  3.     public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
  4.     {
  5.         if (bindingContext.ModelType == typeof(CustomHeaders))
  6.         {
  7.             bindingContext.Model = actionContext.Request.ReadCustomHeaders();
  8.             return true;
  9.         }
  10.         return false;
  11.     }
  12. }

Luego en el controlador debemos indicar que el parámetro de tipo CustomHeaders usa dicho Model Binder:

  1. public class DemoController : ApiController
  2. {
  3.     [Route(«api/demo»)]
  4.     [HttpGet]
  5.     [HttpPost]
  6.     [ResponseType(typeof(CustomHeaders))]
  7.     public IHttpActionResult GetAll([ModelBinder(typeof(CustomHeadersModelBinder))]CustomHeaders ch)
  8.     {
  9.         return Ok(ch);
  10.     }
  11. }

Por supuesto podemos usar un ModelBinderProvider para el tipo CustomHeaders y así evitarnos poner el typeof en el [ModelBinder]:

  1. class CustomHeadersModelBinderProvider : ModelBinderProvider
  2. {
  3.     public override IModelBinder GetBinder(HttpConfiguration configuration, Type modelType)
  4.     {
  5.         if (modelType == typeof(CustomHeaders))
  6.         {
  7.             return new CustomHeadersModelBinder();
  8.         }
  9.         return null;
  10.     }
  11. }

Luego debemos registrarlo en la configuración de WebApi:

  1. config.Services.Insert(typeof(ModelBinderProvider),0, new CustomHeadersModelBinderProvider());

Y ahora el controlador nos queda de la siguiente forma:

  1. public class DemoController : ApiController
  2. {
  3.     [Route(«api/demo»)]
  4.     [HttpGet]
  5.     [HttpPost]
  6.     [ResponseType(typeof(CustomHeaders))]
  7.     public IHttpActionResult GetAll([ModelBinder] CustomHeaders ch)
  8.     {
  9.         return Ok(ch);
  10.     }
  11. }

Observa que sigue siendo necesario decorar el parámetro con [ModelBinder] pero no es necesario especificar el tipo, ya que ya lo hemos dado de alta en la configuración.

En resúmen…

Hemos visto cuatro estrategias para leer datos desde los headers de una petición y acceder a ellos desde nuestros controladores. Como digo el uso de un custom model binder para esto no es conceptualmente correcto y de las otras tres cada una tiene sus ventajas e incovenientes:

  • Message Handler: Bueno para centralizar tareas a realizar en todas las peticiones de Web Api, lleguen estas a un controlador o no.
  • Filtro de acción: Bueno para centralizar tareas a realizar en algunas acciones de uno o más controladores. Si son globales pueden afectar a todas las acciones de todos los controladores.
  • Value Provider: Bueno para añadir orígenes de datos alternativos a los existentes por defecto en Web Api, para enlazar parámetros. De todos modos, asegúrate de entender las reglas del model binding de Web Api que no son, precisamente, sencillas.

Espero que el post te sea interesante!

Saludos!

2 comentarios sobre “WebApi 2–Leer datos desde los headers”

    1. Un middleware OWIN (que sería el equivalente a un MessageHandler siempre que usáramos OWIN), aunque eso afectaría ya no solo a WebApi si no a cualquier petición.

      Y creo que principalmente serían esas.

Responder a Preguntón Cojonero Cabróncete Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *