コード例 #1
0
        /// <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));
        }
コード例 #2
0
        /// <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);
        }
コード例 #3
0
        /// <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));
        }
コード例 #4
0
        /// <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);
        }
コード例 #5
0
        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);
        }