/// <inheritdoc /> public ActionDescriptor SelectBestCandidate(RouteContext context, IReadOnlyList <ActionDescriptor> candidates) { RouteData routeData = context.RouteData; ODataPath odataPath = context.HttpContext.ODataFeature().Path; if (odataPath != null && routeData.Values.ContainsKey(ODataRouteConstants.Action)) { // Get the available parameter names from the route data. Ignore case of key names. IList <string> availableKeys = routeData.Values.Keys.Select(k => k.ToLowerInvariant()).AsList(); // Filter out types we know how to bind out of the parameter lists. These values // do not show up in RouteData() but will bind properly later. var considerCandidates = candidates .Select(c => new ActionIdAndParameters(c.Id, c.Parameters.Count, c.Parameters .Where(p => { return(p.ParameterType != typeof(ODataPath) && !ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.IsODataQueryOptions(p.ParameterType)); }))); // retrieve the optional parameters routeData.Values.TryGetValue(ODataRouteConstants.OptionalParameters, out object wrapper); ODataOptionalParameter optionalWrapper = wrapper as ODataOptionalParameter; // Find the action with the all matched parameters from available keys including // matches with no parameters. Ordered first by the total number of matched // parameters followed by the total number of parameters. Ignore case of // parameter names. The first one is the best match. // // Assume key,relatedKey exist in RouteData. 1st one wins: // Method(ODataPath,ODataQueryOptions) vs Method(ODataPath). // Method(key,ODataQueryOptions) vs Method(key). // Method(key,ODataQueryOptions) vs Method(key). // Method(key,relatedKey) vs Method(key). // Method(key,relatedKey,ODataPath) vs Method(key,relatedKey). var matchedCandidates = considerCandidates .Where(c => !c.FilteredParameters.Any() || TryMatch(c.FilteredParameters, availableKeys, optionalWrapper)) .OrderByDescending(c => c.FilteredParameters.Count) .ThenByDescending(c => c.TotalParameterCount); // Return either the best matched candidate or the first // candidate if none matched. return((matchedCandidates.Any()) ? candidates.Where(c => c.Id == matchedCandidates.FirstOrDefault().Id).FirstOrDefault() : candidates.FirstOrDefault()); } return(_innerSelector.SelectBestCandidate(context, candidates)); }
/// <summary> /// Checks whether the a controller action matches the current route by comparing the parameters /// of the action with the data in the route. /// </summary> /// <param name="context">The current <see cref="RouteContext"/></param> /// <param name="actionDescriptor">The action descriptor</param> /// <param name="parameters">Parameters of the action. This excludes the <see cref="ODataPath"/> and <see cref="Query.ODataQueryOptions"/> parameters</param> /// <param name="availableKeys">The names of the keys found in the uri (entity set keys, related keys, operation parameters)</param> /// <param name="optionalWrapper">Used to check whether a parameter is optional</param> /// <param name="totalParameterCount">Total number of parameters in the action method</param> /// <param name="availableKeysCount">The number of key segments found in the uri. /// This might be less than the size of <paramref name="availableKeys"/> because some keys might have alias names</param> /// <returns></returns> private bool TryMatch( RouteContext context, ActionDescriptor actionDescriptor, IList <ParameterDescriptor> parameters, IList <string> availableKeys, ODataOptionalParameter optionalWrapper, int totalParameterCount, int availableKeysCount) { if (actionDescriptor is ControllerActionDescriptor controllerActionDescriptor) { // if this specific method was selected (e.g. via AttributeRouting) // then we return a match regardless of whether or not the parameters of // the method match the keys in the route if (context.RouteData.Values.TryGetValue(ODataRouteConstants.MethodInfo, out object method) && method is MethodInfo methodInfo) { if (controllerActionDescriptor.MethodInfo == methodInfo) { return(true); } } // if action has [EnableNestedPaths] attribute, then it doesn't // need to match parameters, since this action is expected to // match arbitrarily nested paths even if it doesn't have any parameters if (controllerActionDescriptor.MethodInfo .GetCustomAttributes <EnableNestedPathsAttribute>().Any()) { return(true); } } // navigationProperty is optional in some cases, therefore an action // should not be rejected simply because it does not declare a navigationProperty parameter if (availableKeys.Contains(ODataRouteConstants.NavigationProperty.ToLowerInvariant())) { availableKeysCount -= 1; } // reject action if it doesn't declare a parameter for each segment key // e.g. Get() will be rejected for route /Persons/1 if (totalParameterCount < availableKeysCount) { return(false); } bool matchedBody = false; IDictionary <string, object> conventionsStore = context.HttpContext.ODataFeature().RoutingConventionsStore; // use the parameter name to match. foreach (ParameterDescriptor p in parameters) { string parameterName = p.Name.ToLowerInvariant(); if (availableKeys.Contains(parameterName)) { continue; } if (conventionsStore != null) { // the convention store can contain the parameter as key // with a nested property (e.g. customer.Name) if (conventionsStore.Keys.Any(k => k.Contains(p.Name))) { continue; } } if (context.HttpContext.Request.Query.ContainsKey(p.Name)) { continue; } ControllerParameterDescriptor cP = p as ControllerParameterDescriptor; if (cP != null && optionalWrapper != null) { if (cP.ParameterInfo.IsOptional && optionalWrapper.OptionalParameters.Any(o => o.Name.ToLowerInvariant() == parameterName)) { continue; } } // if we can't find the parameter in the request, check whether // there's a special model binder registered to handle it if (ParameterHasRegisteredModelBinder(p)) { continue; } // if parameter is not bound to a key in the path, // assume that it's bound to the request body // only one parameter should be considered bound to the body if (!matchedBody && RequestHasBody(context)) { matchedBody = true; continue; } return(false); } return(true); }
/// <inheritdoc /> public ActionDescriptor SelectBestCandidate(RouteContext context, IReadOnlyList <ActionDescriptor> candidates) { RouteData routeData = context.RouteData; ODataPath odataPath = context.HttpContext.ODataFeature().Path; IDictionary <string, object> routingConventionsStore = context.HttpContext.ODataFeature().RoutingConventionsStore; if (odataPath != null && routeData.Values.ContainsKey(ODataRouteConstants.Action)) { // Get the available parameter names from the route data. Ignore case of key names. // Remove route prefix and other non-parameter values from availableKeys IList <string> availableKeys = routeData.Values.Keys .Where((key) => !RoutingConventionHelpers.IsRouteParameter(key) && key != ODataRouteConstants.Action && key != ODataRouteConstants.ODataPath && key != ODataRouteConstants.MethodInfo) .Select(k => k.ToLowerInvariant()) .ToList(); int availableKeysCount = 0; if (routingConventionsStore.ContainsKey(ODataRouteConstants.KeyCount)) { availableKeysCount = (int)routingConventionsStore[ODataRouteConstants.KeyCount]; } // Filter out types we know how to bind out of the parameter lists. These values // do not show up in RouteData() but will bind properly later. IEnumerable <ActionIdAndParameters> considerCandidates = candidates .Select(c => new ActionIdAndParameters( id: c.Id, parameterCount: c.Parameters.Count, filteredParameters: c.Parameters.Where(p => p.ParameterType != typeof(ODataPath) && !ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.IsODataQueryOptions(p.ParameterType) && !IsParameterFromQuery(c as ControllerActionDescriptor, p.Name)), descriptor: c)); // retrieve the optional parameters routeData.Values.TryGetValue(ODataRouteConstants.OptionalParameters, out object wrapper); ODataOptionalParameter optionalWrapper = wrapper as ODataOptionalParameter; // Find the action with the all matched parameters from available keys including // matches with no parameters and matches with parameters bound to the body. // Ordered first by the total number of matched // parameters followed by the total number of parameters. Ignore case of // parameter names. The first one is the best match. // // Assume key,relatedKey exist in RouteData. 1st one wins: // Method(ODataPath,ODataQueryOptions) vs Method(ODataPath). // Method(key,ODataQueryOptions) vs Method(key). // Method(key,ODataQueryOptions) vs Method(key). // Method(key,relatedKey) vs Method(key). // Method(key,relatedKey,ODataPath) vs Method(key,relatedKey). List <ActionIdAndParameters> matchedCandidates = considerCandidates .Where(c => TryMatch(context, c.ActionDescriptor, c.FilteredParameters, availableKeys, optionalWrapper, c.TotalParameterCount, availableKeysCount)) .OrderByDescending(c => c.FilteredParameters.Count) .ThenByDescending(c => c.TotalParameterCount) .ToList(); // if there are still multiple candidate actions at this point, let's try some tie-breakers if (matchedCandidates.Count > 1) { // prioritize actions with explicit [ODataRoute] (i.e. attribute routing) ActionIdAndParameters bestCandidate = matchedCandidates.FirstOrDefault(candidate => candidate.ActionDescriptor is ControllerActionDescriptor action && action.MethodInfo.GetCustomAttributes <ODataRouteAttribute>().Any()); if (bestCandidate != null) { return(bestCandidate.ActionDescriptor); } // Next in priority, actions which explicitly declare the request method // e.g. using [AcceptVerbs("POST")], [HttpPost], etc. bestCandidate = matchedCandidates.FirstOrDefault(candidate => ActionAcceptsMethod(candidate.ActionDescriptor as ControllerActionDescriptor, context.HttpContext.Request.Method)); if (bestCandidate != null) { return(bestCandidate.ActionDescriptor); } // also priorize actions that have the exact number of parameters as available keys // this helps disambiguate between overloads of actions that implement actions // e.g. DoSomething(int key) vs DoSomething(), if there are no availableKeys, the // selector could still think that the `int key` param will come from the request body // and end up returning DoSomething(int key) instead of DoSomething() bestCandidate = matchedCandidates.FirstOrDefault(candidate => candidate.FilteredParameters.Count() == availableKeysCount); if (bestCandidate != null) { return(bestCandidate.ActionDescriptor); } } return(matchedCandidates.Select(c => c.ActionDescriptor).FirstOrDefault()); } return(_innerSelector.SelectBestCandidate(context, candidates)); }
/// <summary> /// Checks whether the a controller action matches the current route by comparing the parameters /// of the action with the data in the route. /// </summary> /// <param name="context">The current <see cref="RouteContext"/></param> /// <param name="parameters">Parameters of the action. This excludes the <see cref="ODataPath"/> and <see cref="Query.ODataQueryOptions"/> parameters</param> /// <param name="availableKeys">The names of the keys found in the uri (entity set keys, related keys, operation parameters)</param> /// <param name="optionalWrapper">Used to check whether a parameter is optional</param> /// <param name="totalParameterCount">Total number of parameters in the action method</param> /// <param name="availableKeysCount">The number of key segments found in the uri. /// This might be less than the size of <paramref name="availableKeys"/> because some keys might have alias names</param> /// <returns></returns> private bool TryMatch( RouteContext context, IList <ParameterDescriptor> parameters, IList <string> availableKeys, ODataOptionalParameter optionalWrapper, int totalParameterCount, int availableKeysCount) { // navigationProperty is optional in some cases, therefore an action // should not be rejected simply because it does not declare a navigationProperty parameter if (availableKeys.Contains(ODataRouteConstants.NavigationProperty.ToLowerInvariant())) { availableKeysCount -= 1; } // reject action if it doesn't declare a parameter for each segment key // e.g. Get() will be rejected for route /Persons/1 if (totalParameterCount < availableKeysCount) { return(false); } bool matchedBody = false; var conventionsStore = context.HttpContext.ODataFeature().RoutingConventionsStore; // use the parameter name to match. foreach (var p in parameters) { string parameterName = p.Name.ToLowerInvariant(); if (availableKeys.Contains(parameterName)) { continue; } if (conventionsStore != null) { // the convention store can contain the parameter as key // with a nested property (e.g. customer.Name) if (conventionsStore.Keys.Any(k => k.Contains(p.Name))) { continue; } } if (context.HttpContext.Request.Query.ContainsKey(p.Name)) { continue; } ControllerParameterDescriptor cP = p as ControllerParameterDescriptor; if (cP != null && optionalWrapper != null) { if (cP.ParameterInfo.IsOptional && optionalWrapper.OptionalParameters.Any(o => o.Name.ToLowerInvariant() == parameterName)) { continue; } } // if we can't find the parameter in the request, check whether // there's a special model binder registered to handle it if (ParameterHasRegisteredModelBinder(p)) { continue; } // if parameter is not bound to a key in the path, // assume that it's bound to the request body // only one parameter should be considered bound to the body if (!matchedBody && RequestHasBody(context)) { matchedBody = true; continue; } return(false); } return(true); }
private bool TryMatch(IList <ParameterDescriptor> parameters, IList <string> availableKeys, ODataOptionalParameter optionalWrapper) { // use the parameter name to match. foreach (var p in parameters) { string parameterName = p.Name.ToLowerInvariant(); if (availableKeys.Contains(parameterName)) { continue; } ControllerParameterDescriptor cP = p as ControllerParameterDescriptor; if (cP != null && optionalWrapper != null) { if (cP.ParameterInfo.IsOptional && optionalWrapper.OptionalParameters.Any(o => o.Name.ToLowerInvariant() == parameterName)) { continue; } } return(false); } return(true); }