35

Estoy creando un servicio usando ASP.NET WebApi. Quiero añadir soporte para la negociación del tipo de contenido basado en extensiones en el URI, así que he añadido lo siguiente al código de inicialización del servicio:

public static class WebApiConfig
{
  public static void Register(HttpConfiguration config)
  {
    config.Formatters.JsonFormatter.AddUriPathExtensionMapping("json", "application/json");
    config.Formatters.XmlFormatter.AddUriPathExtensionMapping("xml", "application/xml");
  }
}

Para que esto funcione necesito crear dos rutas para cada acción del controlador (estoy usando exclusivamente enrutamiento basado en atributos):

[Route("item/{id}/details")]
[Route("item/{id}/details.{ext}")]
[HttpGet]
public ItemDetail[] GetItemDetails(int id)
{
  return itemsService.GetItemDetails(id);
}

[Route("item/{name}")]
[Route("item/{name}.{ext}")]
[HttpPost]
public int CreateItem(string name)
{
  return itemsService.Create(name);
}

Esto queda feo y hace que el código sea innecesariamente largo, así que investigué una forma de añadir la ruta con la extensión, automáticamente cuando se crea la ruta normal. Como resultado desarrollé una implementación personalizada de IDirectRouteProvider que puedo usar al registrar los atributos de ruta:

config.MapHttpAttributeRoutes(new AutomaticExtensionRouteProvider());

El código del proveedor personalizado es:

public class AutomaticExtensionRouteProvider : DefaultDirectRouteProvider
{
    protected override IReadOnlyList<RouteEntry> GetActionDirectRoutes(
      HttpActionDescriptor actionDescriptor,
      IReadOnlyList<IDirectRouteFactory> factories,
      IInlineConstraintResolver constraintResolver)
    {
        var result = base.GetActionDirectRoutes(actionDescriptor, factories, constraintResolver);
        var list = new List<RouteEntry>(result);
        foreach(var route in result.Where(r => !r.Route.RouteTemplate.EndsWith(".{ext}")))
        {
            var newTemplate = route.Route.RouteTemplate + ".{ext}";
            if (!result.Any(r => r.Route.RouteTemplate == newTemplate))
            {
                var entry = new RouteEntry(null, new HttpRoute(newTemplate,
                    new HttpRouteValueDictionary(route.Route.Defaults),
                    new HttpRouteValueDictionary(route.Route.Constraints),
                    new HttpRouteValueDictionary(route.Route.DataTokens)));
                list.Add(entry);
            }
        }
        return list.AsReadOnly();
    }
}

El problema es que esto funciona bien, pero falla en un caso: Cuando la última parte de la ruta es un parámetro sin restricciones. Así pues, en el ejemplo anterior, la creación de rutas para GetItemDetails funciona, pero para CreateItem lanza lo siguiente:

System.InvalidOperationException: Multiple actions were found that match the request: 
CreateItem on type FooBar.Api.Controllers.ItemsController
CreateItem on type FooBar.Api.Controllers.ItemsController
   at System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.SelectAction(HttpControllerContext controllerContext)
   at System.Web.Http.Controllers.ApiControllerActionSelector.SelectAction(HttpControllerContext controllerContext)

Me imagino de dónde puede venir el problema: Una cadena arbitraria cumple los patrones {name} y {name}.{ext}, por lo que el motor de WebApi se confunde al intentar seleccionar la ruta adecuada. Pero entonces, ¿Por qué funciona cuando se especifican las dos rutas explícitamente en atributos? Por lo que yo entiendo, la ruta que yo creo en la clase AutomaticExtensionRouteProvider es idéntica a la que se crea explícitamente con un atributo (y si depuro veo que en efecto así es).

Así pues, ¿Qué está pasando aquí?

Konamiman
  • 5,068
  • 2
  • 21
  • 44

1 Answers1

28

He encontrado la solución.

Resulta que las rutas tienen asignada una precedencia numérica asignada, que el motor de enrutamiento de WebApi usa para decidir qué ruta usar en caso de conflicto. Las rutas creadas automáticamente para una misma acción siempre tienen una precedencia distinta, ¡pero la ruta que yo estaba creando manualmente tenía la misma precedencia que la ya existente!

Así pues la solución es añadir lo siguiente a GetActionDirectRoutes, inmediatamente después de new RouteEntry:

entry.Route.DataTokens["precedence"] = 
    ((decimal)route.Route.DataTokens["precedence"]) - 0.1M;
Konamiman
  • 5,068
  • 2
  • 21
  • 44