/// <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)); }
bool TryMatch( HttpContext context, ActionIdAndParameters action, ISet <string> availableKeys, IDictionary <string, object> conventionsStore, IReadOnlyList <IEdmOptionalParameter> optionalParameters, int availableKeysCount) { var parameters = action.FilteredParameters; var totalParameterCount = action.TotalParameterCount; if (availableKeys.Contains(ODataRouteConstants.NavigationProperty)) { availableKeysCount -= 1; } if (totalParameterCount < availableKeysCount) { return(false); } var matchedBody = false; var keys = conventionsStore.Keys.ToArray(); for (var i = 0; i < parameters.Count; i++) { var parameter = parameters[i]; var parameterName = parameter.Name; if (availableKeys.Contains(parameterName)) { continue; } var matchesKey = false; for (var j = 0; j < keys.Length; j++) { if (keys[j].Contains(parameterName, StringComparison.Ordinal)) { matchesKey = true; break; } } if (matchesKey) { continue; } if (context.Request.Query.ContainsKey(parameterName)) { continue; } if (parameter is ControllerParameterDescriptor param && optionalParameters.Count > 0) { if (param.ParameterInfo.IsOptional && optionalParameters.Any(p => string.Equals(p.Name, parameterName, StringComparison.OrdinalIgnoreCase))) { continue; } } if (ParameterHasRegisteredModelBinder(parameter)) { continue; } if (!matchedBody && RequestHasBody(context)) { matchedBody = true; continue; } return(false); } return(true); }