Example #1
0
        /// <summary>
        /// Returns the set of best matching actions.
        /// </summary>
        /// <param name="context">The <see cref="ActionSelectionContext">context</see> to select the actions from.</param>
        /// <returns>A <see cref="IReadOnlyList{T}">read-only list</see> of the best matching <see cref="ActionDescriptor">actions</see>.</returns>
        protected virtual IReadOnlyList <ActionDescriptor> SelectBestActions(ActionSelectionContext context)
        {
            Arg.NotNull(context, nameof(context));

            var bestMatches = new List <ActionDescriptor>(context.MatchingActions.Count);

            bestMatches.AddRange(MatchVersionNeutralActions(context));

            if (context.RequestedVersion == null)
            {
                if (!Options.AssumeDefaultVersionWhenUnspecified)
                {
                    return(bestMatches);
                }

                context.RequestedVersion = ApiVersionSelector.SelectVersion(context.HttpContext.Request, context.AllVersions);

                if (context.RequestedVersion == null)
                {
                    return(bestMatches);
                }
            }

            var implicitMatches = new List <ActionDescriptor>();
            var explicitMatches = from action in context.MatchingActions
                                  let model = action.GetProperty <ApiVersionModel>()
                                              where ActionIsSatisfiedBy(action, model, context.RequestedVersion, implicitMatches)
                                              select action;

            bestMatches.AddRange(explicitMatches);

            if (bestMatches.Count == 0)
            {
                bestMatches.AddRange(implicitMatches);
            }

            if (bestMatches.Count != 1)
            {
                return(bestMatches);
            }

            if (bestMatches[0].IsApiVersionNeutral())
            {
                bestMatches.AddRange(implicitMatches);
            }

            return(bestMatches);
        }
Example #2
0
        /// <summary>
        /// Selects the best action given the provided route context and list of candidate actions.
        /// </summary>
        /// <param name="context">The current <see cref="RouteContext">route context</see> to evaluate.</param>
        /// <param name="candidates">The <see cref="IReadOnlyList{T}">read-only list</see> of candidate <see cref="ActionDescriptor">actions</see> to select from.</param>
        /// <returns>The best candidate <see cref="ActionDescriptor">action</see> or <c>null</c> if no candidate matches.</returns>
        public override ActionDescriptor SelectBestCandidate(RouteContext context, IReadOnlyList <ActionDescriptor> candidates)
        {
            Arg.NotNull(context, nameof(context));
            Arg.NotNull(candidates, nameof(candidates));

            var httpContext         = context.HttpContext;
            var odataRouteCandidate = httpContext.ODataFeature().Path != null;

            if (!odataRouteCandidate)
            {
                return(base.SelectBestCandidate(context, candidates));
            }

            if (IsRequestedApiVersionAmbiguous(context, out var apiVersion))
            {
                return(null);
            }

            var matches          = EvaluateActionConstraints(context, candidates);
            var selectionContext = new ActionSelectionContext(httpContext, matches, apiVersion);
            var bestActions      = SelectBestActions(selectionContext);
            var finalMatch       = bestActions.Select(action => new ActionCandidate(action))
                                   .OrderByDescending(candidate => candidate.FilteredParameters.Count)
                                   .ThenByDescending(candidate => candidate.TotalParameterCount)
                                   .FirstOrDefault()?.Action;
            IReadOnlyList <ActionDescriptor> finalMatches = finalMatch == null?Array.Empty <ActionDescriptor>() : new[] { finalMatch };
            var feature         = httpContext.Features.Get <IApiVersioningFeature>();
            var selectionResult = feature.SelectionResult;

            feature.RequestedApiVersion = selectionContext.RequestedVersion;
            selectionResult.AddCandidates(candidates);

            if (finalMatches.Count == 0)
            {
                return(null);
            }

            selectionResult.AddMatches(finalMatches);

            var bestCandidate = RoutePolicy.Evaluate(context, selectionResult);

            if (bestCandidate is ControllerActionDescriptor controllerAction)
            {
                context.RouteData.Values[ActionKey] = controllerAction.ActionName;
            }

            return(bestCandidate);
        }
        private BadRequestHandler IsValidRequest(ActionSelectionContext context, IReadOnlyList <ActionDescriptor> candidates)
        {
            Contract.Requires(context != null);
            Contract.Requires(candidates != null);

            if (!context.MatchingActions.Any() && !candidates.Any())
            {
                return(null);
            }

            var code             = default(string);
            var requestedVersion = default(string);
            var parsedVersion    = context.RequestedVersion;
            var actionNames      = new Lazy <string>(() => Join(NewLine, context.MatchingActions.Select(a => a.DisplayName)));

            if (parsedVersion == null)
            {
                requestedVersion = context.HttpContext.GetRawRequestedApiVersion();

                if (IsNullOrEmpty(requestedVersion))
                {
                    code = "ApiVersionUnspecified";
                    logger.ApiVersionUnspecified(actionNames.Value);
                    return(new BadRequestHandler(Options, code, SR.ApiVersionUnspecified));
                }
                else if (TryParse(requestedVersion, out parsedVersion))
                {
                    code = "UnsupportedApiVersion";
                    logger.ApiVersionUnmatched(parsedVersion, actionNames.Value);
                }
                else
                {
                    code = "InvalidApiVersion";
                    logger.ApiVersionInvalid(requestedVersion);
                }
            }
            else
            {
                requestedVersion = parsedVersion.ToString();
                code             = "UnsupportedApiVersion";
                logger.ApiVersionUnmatched(parsedVersion, actionNames.Value);
            }

            var message = SR.VersionedResourceNotSupported.FormatDefault(context.HttpContext.Request.GetDisplayUrl(), requestedVersion);

            return(new BadRequestHandler(Options, code, message));
        }
Example #4
0
        /// <summary>
        /// Selects the best action given the provided route context and list of candidate actions.
        /// </summary>
        /// <param name="context">The current <see cref="RouteContext">route context</see> to evaluate.</param>
        /// <param name="candidates">The <see cref="IReadOnlyList{T}">read-only list</see> of candidate <see cref="ActionDescriptor">actions</see> to select from.</param>
        /// <returns>The best candidate <see cref="ActionDescriptor">action</see> or <c>null</c> if no candidate matches.</returns>
        public virtual ActionDescriptor SelectBestCandidate(RouteContext context, IReadOnlyList <ActionDescriptor> candidates)
        {
            Arg.NotNull(context, nameof(context));
            Arg.NotNull(candidates, nameof(candidates));

            var httpContext           = context.HttpContext;
            var apiVersion            = default(ApiVersion);
            var invalidRequestHandler = default(RequestHandler);

            if ((invalidRequestHandler = VerifyRequestedApiVersionIsNotAmbiguous(httpContext, out apiVersion)) != null)
            {
                context.Handler = invalidRequestHandler;
                return(null);
            }

            var matches          = EvaluateActionConstraints(context, candidates);
            var selectionContext = new ActionSelectionContext(httpContext, matches, apiVersion);
            var finalMatches     = SelectBestActions(selectionContext);

            if (finalMatches == null || finalMatches.Count == 0)
            {
                if ((invalidRequestHandler = IsValidRequest(selectionContext, candidates)) != null)
                {
                    context.Handler = invalidRequestHandler;
                }

                return(null);
            }
            else if (finalMatches.Count == 1)
            {
                var selectedAction = finalMatches[0];
                selectedAction.AggregateAllVersions(selectionContext);
                httpContext.ApiVersionProperties().ApiVersion = selectionContext.RequestedVersion;
                return(selectedAction);
            }
            else
            {
                var actionNames = Join(NewLine, finalMatches.Select(a => a.DisplayName));

                logger.AmbiguousActions(actionNames);

                var message = SR.ActionSelector_AmbiguousActions.FormatDefault(NewLine, actionNames);

                throw new AmbiguousActionException(message);
            }
        }
        /// <summary>
        /// Selects the best action given the provided route context and list of candidate actions.
        /// </summary>
        /// <param name="context">The current <see cref="RouteContext">route context</see> to evaluate.</param>
        /// <param name="candidates">The <see cref="IReadOnlyList{T}">read-only list</see> of candidate <see cref="ActionDescriptor">actions</see> to select from.</param>
        /// <returns>The best candidate <see cref="ActionDescriptor">action</see> or <c>null</c> if no candidate matches.</returns>
        public virtual ActionDescriptor SelectBestCandidate(RouteContext context, IReadOnlyList <ActionDescriptor> candidates)
        {
            Arg.NotNull(context, nameof(context));
            Arg.NotNull(candidates, nameof(candidates));

            var httpContext = context.HttpContext;

            if ((context.Handler = VerifyRequestedApiVersionIsNotAmbiguous(httpContext, out var apiVersion)) != null)
            {
                return(null);
            }

            var matches        = EvaluateActionConstraints(context, candidates);
            var selectedAction = SelectActionWithoutApiVersionConvention(matches);

            if (selectedAction != null)
            {
                return(selectedAction);
            }

            var selectionContext = new ActionSelectionContext(httpContext, matches, apiVersion);
            var finalMatches     = SelectBestActions(selectionContext);
            var properties       = httpContext.ApiVersionProperties();
            var selectionResult  = properties.SelectionResult;

            properties.ApiVersion = selectionContext.RequestedVersion;
            selectionResult.AddCandidates(candidates);

            if (finalMatches != null)
            {
                if ((selectedAction = SelectActionWithApiVersionPolicyApplied(finalMatches, selectionResult)) == null)
                {
                    AppendPossibleMatches(finalMatches, context, selectionResult);
                }
                else
                {
                    AppendPossibleMatches(new[] { selectedAction }, context, selectionResult);
                    return(selectedAction);
                }
            }

            // note: even though we may have had a successful match, this method could be called multiple times. the final decision
            // is made by the IApiVersionRoutePolicy. we return here to make sure all candidates have been considered at least once.
            selectionResult.EndIteration();
            return(null);
        }
        /// <summary>
        /// Selects the best action given the provided route context and list of candidate actions.
        /// </summary>
        /// <param name="context">The current <see cref="RouteContext">route context</see> to evaluate.</param>
        /// <param name="candidates">The <see cref="IReadOnlyList{T}">read-only list</see> of candidate <see cref="ActionDescriptor">actions</see> to select from.</param>
        /// <returns>The best candidate <see cref="ActionDescriptor">action</see> or <c>null</c> if no candidate matches.</returns>
        public override ActionDescriptor SelectBestCandidate(RouteContext context, IReadOnlyList <ActionDescriptor> candidates)
        {
            Arg.NotNull(context, nameof(context));
            Arg.NotNull(candidates, nameof(candidates));

            var httpContext         = context.HttpContext;
            var odataRouteCandidate = httpContext.ODataFeature().Path != null;

            if (!odataRouteCandidate)
            {
                return(base.SelectBestCandidate(context, candidates));
            }

            if (IsRequestedApiVersionAmbiguous(context, out var apiVersion))
            {
                return(null);
            }

            var matches          = EvaluateActionConstraints(context, candidates);
            var selectionContext = new ActionSelectionContext(httpContext, matches, apiVersion);
            var bestActions      = SelectBestActions(selectionContext);
            var finalMatches     = bestActions.Select(action => new ActionCandidate(action))
                                   .OrderByDescending(candidate => candidate.FilteredParameters.Count)
                                   .ThenByDescending(candidate => candidate.TotalParameterCount)
                                   .Take(1)
                                   .Select(candidate => candidate.Action)
                                   .ToArray();
            var feature         = httpContext.Features.Get <IApiVersioningFeature>();
            var selectionResult = feature.SelectionResult;

            feature.RequestedApiVersion = selectionContext.RequestedVersion;
            selectionResult.AddCandidates(candidates);

            if (finalMatches.Length == 0)
            {
                return(null);
            }

            selectionResult.AddMatches(finalMatches);

            return(RoutePolicy.Evaluate(context, selectionResult));
        }
        /// <summary>
        /// Selects the best action given the provided route context and list of candidate actions.
        /// </summary>
        /// <param name="context">The current <see cref="RouteContext">route context</see> to evaluate.</param>
        /// <param name="candidates">The <see cref="IReadOnlyList{T}">read-only list</see> of candidate <see cref="ActionDescriptor">actions</see> to select from.</param>
        /// <returns>The best candidate <see cref="ActionDescriptor">action</see> or <c>null</c> if no candidate matches.</returns>
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
        public virtual ActionDescriptor?SelectBestCandidate(RouteContext context, IReadOnlyList <ActionDescriptor> candidates)
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            var httpContext = context.HttpContext;

            if (IsRequestedApiVersionAmbiguous(context, out var apiVersion))
            {
                return(null);
            }

            var matches        = EvaluateActionConstraints(context, candidates);
            var selectedAction = SelectActionWithoutApiVersionConvention(matches);

            if (selectedAction != null)
            {
                return(selectedAction);
            }

            var selectionContext = new ActionSelectionContext(httpContext, matches, apiVersion);
            var finalMatches     = SelectBestActions(selectionContext);
            var feature          = httpContext.Features.Get <IApiVersioningFeature>();
            var selectionResult  = feature.SelectionResult;

            feature.RequestedApiVersion = selectionContext.RequestedVersion;
            selectionResult.AddCandidates(candidates);

            if (finalMatches.Count == 0)
            {
                return(null);
            }

            selectionResult.AddMatches(finalMatches);

            return(RoutePolicy.Evaluate(context, selectionResult));
        }
        /// <summary>
        /// Selects the best action given the provided route context and list of candidate actions.
        /// </summary>
        /// <param name="context">The current <see cref="RouteContext">route context</see> to evaluate.</param>
        /// <param name="candidates">The <see cref="IReadOnlyList{T}">read-only list</see> of candidate <see cref="ActionDescriptor">actions</see> to select from.</param>
        /// <returns>The best candidate <see cref="ActionDescriptor">action</see> or <c>null</c> if no candidate matches.</returns>
        public virtual ActionDescriptor?SelectBestCandidate(RouteContext context, IReadOnlyList <ActionDescriptor> candidates)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            var httpContext = context.HttpContext;

            if (IsRequestedApiVersionAmbiguous(context, out var apiVersion))
            {
                return(null);
            }

            var matches        = EvaluateActionConstraints(context, candidates);
            var selectedAction = SelectActionWithoutApiVersionConvention(matches);

            if (selectedAction != null)
            {
                return(selectedAction);
            }

            var selectionContext = new ActionSelectionContext(httpContext, matches, apiVersion);
            var finalMatches     = SelectBestActions(selectionContext);
            var feature          = httpContext.ApiVersioningFeature();
            var selectionResult  = feature.SelectionResult;

            feature.RequestedApiVersion = selectionContext.RequestedVersion;
            selectionResult.AddCandidates(candidates);

            if (finalMatches.Count == 0)
            {
                return(null);
            }

            selectionResult.AddMatches(finalMatches);

            return(RoutePolicy.Evaluate(context, selectionResult));
        }
Example #9
0
 private static IEnumerable <ActionDescriptor> MatchVersionNeutralActions(ActionSelectionContext context) =>
 from action in context.MatchingActions
 let model = action.GetProperty <ApiVersionModel>()
                 where model?.IsApiVersionNeutral ?? false
             select action;
Example #10
0
        private RequestHandler IsValidRequest(ActionSelectionContext context, IReadOnlyList <ActionDescriptor> candidates)
        {
            Contract.Requires(context != null);
            Contract.Requires(candidates != null);

            if (!context.MatchingActions.Any() && !candidates.Any())
            {
                return(null);
            }

            var code             = default(string);
            var requestedVersion = default(string);
            var parsedVersion    = context.RequestedVersion;
            var actionNames      = new Lazy <string>(() => Join(NewLine, candidates.Select(a => a.DisplayName)));
            var allowedMethods   = new Lazy <HashSet <string> >(
                () => new HashSet <string>(candidates.SelectMany(c => c.ActionConstraints.OfType <HttpMethodActionConstraint>())
                                           .SelectMany(ac => ac.HttpMethods),
                                           StringComparer.OrdinalIgnoreCase));
            var newRequestHandler = default(Func <ApiVersioningOptions, string, string, RequestHandler>);

            if (parsedVersion == null)
            {
                requestedVersion = context.HttpContext.ApiVersionProperties().RawApiVersion;

                if (IsNullOrEmpty(requestedVersion))
                {
                    code = "ApiVersionUnspecified";
                    logger.ApiVersionUnspecified(actionNames.Value);
                    return(new BadRequestHandler(Options, code, SR.ApiVersionUnspecified));
                }
                else if (TryParse(requestedVersion, out parsedVersion))
                {
                    code = "UnsupportedApiVersion";
                    logger.ApiVersionUnmatched(parsedVersion, actionNames.Value);

                    if (allowedMethods.Value.Contains(context.HttpContext.Request.Method))
                    {
                        newRequestHandler = (o, c, m) => new BadRequestHandler(o, c, m);
                    }
                    else
                    {
                        newRequestHandler = (o, c, m) => new MethodNotAllowedHandler(o, c, m, allowedMethods.Value.ToArray());
                    }
                }
                else
                {
                    code = "InvalidApiVersion";
                    logger.ApiVersionInvalid(requestedVersion);
                    newRequestHandler = (o, c, m) => new BadRequestHandler(o, c, m);
                }
            }
            else
            {
                requestedVersion = parsedVersion.ToString();
                code             = "UnsupportedApiVersion";
                logger.ApiVersionUnmatched(parsedVersion, actionNames.Value);

                if (allowedMethods.Value.Contains(context.HttpContext.Request.Method))
                {
                    newRequestHandler = (o, c, m) => new BadRequestHandler(o, c, m);
                }
                else
                {
                    newRequestHandler = (o, c, m) => new MethodNotAllowedHandler(o, c, m, allowedMethods.Value.ToArray());
                }
            }

            var message = SR.VersionedResourceNotSupported.FormatDefault(context.HttpContext.Request.GetDisplayUrl(), requestedVersion);

            return(newRequestHandler(Options, code, message));
        }
Example #11
0
        /// <inheritdoc />
        public override ActionDescriptor?SelectBestCandidate(RouteContext context, IReadOnlyList <ActionDescriptor> candidates)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (candidates == null)
            {
                throw new ArgumentNullException(nameof(candidates));
            }

            var httpContext         = context.HttpContext;
            var odata               = httpContext.ODataFeature();
            var odataRouteCandidate = odata.Path != null;

            if (!odataRouteCandidate)
            {
                return(base.SelectBestCandidate(context, candidates));
            }

            if (IsRequestedApiVersionAmbiguous(context, out var apiVersion))
            {
                return(null);
            }

            var matches          = EvaluateActionConstraints(context, candidates);
            var selectionContext = new ActionSelectionContext(httpContext, matches, apiVersion);
            var bestActions      = SelectBestActions(selectionContext);
            var finalMatches     = Array.Empty <ActionDescriptor>();

            if (bestActions.Count > 0)
            {
                // REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNetCore.OData/Routing/ODataActionSelector.cs
                var routeValues        = context.RouteData.Values;
                var conventionsStore   = odata.RoutingConventionsStore ?? new Dictionary <string, object>(capacity: 0);
                var availableKeys      = new HashSet <string>(routeValues.Keys.Where(IsAvailableKey), StringComparer.OrdinalIgnoreCase);
                var availableKeysCount = conventionsStore.TryGetValue(ODataRouteConstants.KeyCount, out var v) ? (int)v : 0;
                var possibleCandidates = bestActions.Select(candidate => new ActionIdAndParameters(candidate, ParameterHasRegisteredModelBinder));
                var optionalParameters = routeValues.TryGetValue(ODataRouteConstants.OptionalParameters, out var wrapper) ?
                                         GetOptionalParameters(wrapper) :
                                         Array.Empty <IEdmOptionalParameter>();
                var matchedCandidates = possibleCandidates
                                        .Where(c => TryMatch(httpContext, c, availableKeys, conventionsStore, optionalParameters, availableKeysCount))
                                        .OrderByDescending(c => c.FilteredParameters.Count)
                                        .ThenByDescending(c => c.TotalParameterCount)
                                        .ToArray();

                if (matchedCandidates.Length == 1)
                {
                    finalMatches = new[] { matchedCandidates[0].Action };
                }
                else if (matchedCandidates.Length > 1)
                {
                    var results = matchedCandidates.Where(c => ActionAcceptsMethod(c.Action, httpContext.Request.Method)).ToArray();

                    finalMatches = results.Length switch
                    {
                        0 => matchedCandidates.Where(c => c.FilteredParameters.Count == availableKeysCount).Select(c => c.Action).ToArray(),
                        1 => new[] { results[0].Action },
                        _ => results.Where(c => c.FilteredParameters.Count == availableKeysCount).Select(c => c.Action).ToArray(),
                    };
                }
            }

            var feature         = httpContext.Features.Get <IApiVersioningFeature>();
            var selectionResult = feature.SelectionResult;

            feature.RequestedApiVersion = selectionContext.RequestedVersion;
            selectionResult.AddCandidates(candidates);

            if (finalMatches.Length > 0)
            {
                selectionResult.AddMatches(finalMatches);
            }
            else
            {
                // OData endpoint routing calls back through IActionSelector. if endpoint routing is enabled
                // then the answer is final; proceed to route policy. if classic routing, it's possible the
                // IActionSelector will be entered again
                if (!UsingEndpointRouting)
                {
                    return(null);
                }
            }

            return(RoutePolicy.Evaluate(context, selectionResult));
        }