/// <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); }
/// <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)); }
/// <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)); }
private static IEnumerable <ActionDescriptor> MatchVersionNeutralActions(ActionSelectionContext context) => from action in context.MatchingActions let model = action.GetProperty <ApiVersionModel>() where model?.IsApiVersionNeutral ?? false select action;
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)); }
/// <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)); }