Reputation: 13391
I'm using ASP.NET Web API 2 with attribute routing but i can't seem to get the versioning using media types application/vnd.company[.version].param[+json]
to work.
I get the following error:
The given key was not present in the dictionary.
which originates from testing the key _actionParameterNames[descriptor]
in FindActionMatchRequiredRouteAndQueryParameters()
method.
foreach (var candidate in candidatesFound)
{
HttpActionDescriptor descriptor = candidate.ActionDescriptor;
if (IsSubset(_actionParameterNames[descriptor], candidate.CombinedParameterNames))
{
matches.Add(candidate);
}
}
Source: ApiControllerActionSelector.cs
After further debugging I've realized that if you have two controllers
[RoutePrefix("api/people")]
public class PeopleController : BaseApiController
{
[Route("")]
public HttpResponseMessage GetPeople()
{
}
[Route("identifier/{id}")]
public HttpResponseMessage GetPersonById()
{
}
}
[RoutePrefix("api/people")]
public class PeopleV2Controller : BaseApiController
{
[Route("")]
public HttpResponseMessage GetPeople()
{
}
[Route("identifier/{id}")]
public HttpResponseMessage GetPersonById()
{
}
}
you can't use your custom ApiVersioningSelector : DefaultHttpControllerSelector
because it will test the keys,as stated above, from all controllers having the same [RoutePrefix("api/people")]
and obviously an exception will be thrown.
Just to be sure the right controller was selected
I don't know if this is a bug, but using route [RoutePrefix("api/v1/people")] to version API
makes me sad.
NOTE: This works great without attribute routing.
UPDATE
public class ApiVersioningSelector : DefaultHttpControllerSelector
{
private HttpConfiguration _HttpConfiguration;
public ApiVersioningSelector(HttpConfiguration httpConfiguration)
: base(httpConfiguration)
{
_HttpConfiguration = httpConfiguration;
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
IDictionary<string, HttpControllerDescriptor> controllers = GetControllerMapping();
var attributedRoutesData = request.GetRouteData().GetSubRoutes();
var subRouteData = attributedRoutesData.LastOrDefault(); //LastOrDefault() will get PeopleController, FirstOrDefault will get People{version}Controller which we don't want
var actions = (ReflectedHttpActionDescriptor[])subRouteData.Route.DataTokens["actions"];
var controllerName = actions[0].ControllerDescriptor.ControllerName;
//For controller name without attribute routing
//var controllerName = (string)routeData.Values["controller"];
HttpControllerDescriptor oldControllerDescriptor;
if (controllers.TryGetValue(controllerName, out oldControllerDescriptor))
{
var apiVersion = GetVersionFromMediaType(request);
var newControllerName = String.Concat(controllerName, "V", apiVersion);
HttpControllerDescriptor newControllerDescriptor;
if (controllers.TryGetValue(newControllerName, out newControllerDescriptor))
{
return newControllerDescriptor;
}
return oldControllerDescriptor;
}
return null;
}
private string GetVersionFromMediaType(HttpRequestMessage request)
{
var acceptHeader = request.Headers.Accept;
var regularExpression = new Regex(@"application\/vnd\.mycompany\.([a-z]+)\.v([0-9]+)\+json",
RegexOptions.IgnoreCase);
foreach (var mime in acceptHeader)
{
var match = regularExpression.Match(mime.MediaType);
if (match != null)
{
return match.Groups[2].Value;
}
}
return "1";
}
}
Upvotes: 12
Views: 5384
Reputation: 58009
Thanks for the sharing your code. I have modified your version controller selector like below and tried some scenarios and it seems to work well. Can you try updating your controller selector like below and see if it works?
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
HttpControllerDescriptor controllerDescriptor = null;
// get list of all controllers provided by the default selector
IDictionary<string, HttpControllerDescriptor> controllers = GetControllerMapping();
IHttpRouteData routeData = request.GetRouteData();
if (routeData == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
//check if this route is actually an attribute route
IEnumerable<IHttpRouteData> attributeSubRoutes = routeData.GetSubRoutes();
var apiVersion = GetVersionFromMediaType(request);
if (attributeSubRoutes == null)
{
string controllerName = GetRouteVariable<string>(routeData, "controller");
if (controllerName == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
string newControllerName = String.Concat(controllerName, "V", apiVersion);
if (controllers.TryGetValue(newControllerName, out controllerDescriptor))
{
return controllerDescriptor;
}
else
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
}
else
{
// we want to find all controller descriptors whose controller type names end with
// the following suffix(ex: CustomersV1)
string newControllerNameSuffix = String.Concat("V", apiVersion);
IEnumerable<IHttpRouteData> filteredSubRoutes = attributeSubRoutes.Where(attrRouteData =>
{
HttpControllerDescriptor currentDescriptor = GetControllerDescriptor(attrRouteData);
bool match = currentDescriptor.ControllerName.EndsWith(newControllerNameSuffix);
if (match && (controllerDescriptor == null))
{
controllerDescriptor = currentDescriptor;
}
return match;
});
routeData.Values["MS_SubRoutes"] = filteredSubRoutes.ToArray();
}
return controllerDescriptor;
}
private HttpControllerDescriptor GetControllerDescriptor(IHttpRouteData routeData)
{
return ((HttpActionDescriptor[])routeData.Route.DataTokens["actions"]).First().ControllerDescriptor;
}
// Get a value from the route data, if present.
private static T GetRouteVariable<T>(IHttpRouteData routeData, string name)
{
object result = null;
if (routeData.Values.TryGetValue(name, out result))
{
return (T)result;
}
return default(T);
}
Upvotes: 18