Версии ASP.NET Web API 2 с типами носителей

Я использую ASP.NET Web API 2 с маршрутизацией атрибутов, но я не могу заставить управление версиями использовать типы носителей application/vnd.company[.version].param[+json].

enter image description here

Я получаю следующую ошибку:

Указанный ключ отсутствует в словаре.

который исходит от тестирования ключа _actionParameterNames[descriptor] в FindActionMatchRequiredRouteAndQueryParameters().

foreach (var candidate in candidatesFound)
{
        HttpActionDescriptor descriptor = candidate.ActionDescriptor;
        if (IsSubset(_actionParameterNames[descriptor], candidate.CombinedParameterNames))
        {
            matches.Add(candidate);
        }
}

Источник: ApiControllerActionSelector.cs

После дальнейшей отладки я понял, что если у вас есть два контроллера

[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()
    {
    }
}

вы не можете использовать свой собственный ApiVersioningSelector : DefaultHttpControllerSelector, потому что он проверит ключи, как указано выше, со всех контроллеров, имеющих тот же [RoutePrefix("api/people")] и, очевидно, исключение будет выбрано.

enter image description hereenter image description here

Чтобы убедиться, что выбран правильный контроллер

enter image description here

Я не знаю, является ли это ошибкой, но использование маршрута [RoutePrefix("api/v1/people")] to version API меня огорчает.

ПРИМЕЧАНИЕ: Это отлично работает без маршрутизации атрибутов.

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";
        }

    }

Ответ 1

Спасибо за обмен вашим кодом. Я изменил свой селектор контроллера версии, как показано ниже, и попробовал некоторые сценарии, и, похоже, он работает хорошо. Можете ли вы попробовать обновить свой селектор контроллеров, как показано ниже, и посмотреть, работает ли он?

    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);
    }