public async Task ApplyAsync_HasMatchFindsEndpoint_WithRouteValues() { // Arrange var policy = new DynamicControllerEndpointMatcherPolicy(SelectorCache, Comparer); var endpoints = new[] { DynamicEndpoint, }; var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), }; var scores = new[] { 0, }; var candidates = new CandidateSet(endpoints, values, scores); Transform = (c, values, state) => { return(new ValueTask <RouteValueDictionary>(new RouteValueDictionary(new { controller = "Home", action = "Index", state }))); }; var httpContext = new DefaultHttpContext() { RequestServices = Services, }; // Act await policy.ApplyAsync(httpContext, candidates); // Assert Assert.Same(ControllerEndpoints[0], candidates[0].Endpoint); Assert.Collection( candidates[0].Values.OrderBy(kvp => kvp.Key), kvp => { Assert.Equal("action", kvp.Key); Assert.Equal("Index", kvp.Value); }, kvp => { Assert.Equal("controller", kvp.Key); Assert.Equal("Home", kvp.Value); }, kvp => { Assert.Equal("slug", kvp.Key); Assert.Equal("test", kvp.Value); }, kvp => { Assert.Equal("state", kvp.Key); Assert.Same(State, kvp.Value); }); Assert.True(candidates.IsValidCandidate(0)); }
public void ApplyAsyncSetEndpointInvalidIfEndpointDoesnotHaveODataSelectedActionDescriptor() { // Arrange HttpContext context = new DefaultHttpContext(); Mock <ActionDescriptor> actionDescriptor1 = new Mock <ActionDescriptor>(); context.ODataFeature().ActionDescriptor = actionDescriptor1.Object; Mock <ActionDescriptor> actionDescriptor2 = new Mock <ActionDescriptor>(); CandidateSet candidateSet = CreateCandidateSet(actionDescriptor2.Object); Assert.True(candidateSet.IsValidCandidate(0)); // Guard // Act Task actual = new ODataEndpointSelectorPolicy().ApplyAsync(context, candidateSet); // Assert Assert.Equal(Task.CompletedTask, actual); Assert.False(candidateSet.IsValidCandidate(0)); }
private static bool AllInvalid(CandidateSet candidates) { for (int i = 0; i < candidates.Count; i++) { if (candidates.IsValidCandidate(i)) { return(false); } } return(true); }
public async Task ApplyAsync_CanExpandTheListOfFoundEndpoints() { // Arrange var policy = new DynamicControllerEndpointMatcherPolicy(SelectorCache, Comparer); var endpoints = new[] { DynamicEndpoint, }; var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), }; var scores = new[] { 0, }; var candidates = new CandidateSet(endpoints, values, scores); Transform = (c, values, state) => { return(new ValueTask <RouteValueDictionary>(new RouteValueDictionary(new { controller = "Home", action = "Index", state }))); }; Filter = (c, values, state, endpoints) => new ValueTask <IReadOnlyList <Endpoint> >(new[] { ControllerEndpoints[1], ControllerEndpoints[2] }); var httpContext = new DefaultHttpContext() { RequestServices = Services, }; // Act await policy.ApplyAsync(httpContext, candidates); // Assert Assert.Equal(2, candidates.Count); Assert.True(candidates.IsValidCandidate(0)); Assert.True(candidates.IsValidCandidate(1)); Assert.Same(ControllerEndpoints[1], candidates[0].Endpoint); Assert.Same(ControllerEndpoints[2], candidates[1].Endpoint); }
public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { if (httpContext == null) { throw new ArgumentNullException(nameof(httpContext)); } if (candidates == null) { throw new ArgumentNullException(nameof(candidates)); } for (var i = 0; i < candidates.Count; i++) { if (!candidates.IsValidCandidate(i)) { continue; } ref var candidate = ref candidates[i]; var endpoint = candidate.Endpoint; var page = endpoint.Metadata.GetMetadata <PageActionDescriptor>(); if (page != null) { // We found an endpoint instance that has a PageActionDescriptor, but not a // CompiledPageActionDescriptor. Update the CandidateSet. Task <CompiledPageActionDescriptor> compiled; if (_loader is DefaultPageLoader defaultPageLoader) { compiled = defaultPageLoader.LoadAsync(page, endpoint.Metadata); } else { compiled = _loader.LoadAsync(page); } if (compiled.IsCompletedSuccessfully) { candidates.ReplaceEndpoint(i, compiled.Result.Endpoint, candidate.Values); } else { // In the most common case, GetOrAddAsync will return a synchronous result. // Avoid going async since this is a fairly hot path. return(ApplyAsyncAwaited(candidates, compiled, i)); } } }
private static bool AllInvalid(CandidateSet candidates) { for (int i = 0; i < candidates.Count; i++) { // We have to check if candidates needs to be ignored here // So we dont return false when all endpoints are invalid if (candidates.IsValidCandidate(i) && candidates[i].Endpoint.Metadata.GetMetadata <IgnoreFromNotFoundSelectorPolicyAttribute>() is null) { return(false); } } return(true); }
public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { for (var i = 0; i < candidates.Count; i++) { if (!candidates.IsValidCandidate(i)) { continue; } var metadata = candidates[i].Endpoint.Metadata.GetMetadata <Metadata>(); if (metadata is null) { continue; } // this one is OURS! // // since we own it we can update values in place. var values = candidates[i].Values ?? new RouteValueDictionary(); if (!cache.TryGetValue(metadata, out var result)) { // use whatever criteria you want to match this to an MVC action. We're using an expression/type. result = FindMatchingEndpoint(metadata, dataSource.Endpoints); cache.TryAdd(metadata, result); } // emplace the MVC standard route values to be convincing var action = result.Metadata.GetMetadata <ControllerActionDescriptor>(); foreach (var kvp in action.RouteValues) { if (kvp.Value is string s && s.Length > 0) { values[kvp.Key] = kvp.Value; } } if (result is null) { throw new InvalidOperationException("Derp."); } candidates.ReplaceEndpoint(i, result, values); } return(Task.CompletedTask); }
// This is almost the same as the code in ActionSelector, but we can't really share the logic // because we need to track the index of each candidate - and, each candidate has its own route // values. private IReadOnlyList <(int index, ActionSelectorCandidate candidate)> EvaluateActionConstraints( HttpContext httpContext, CandidateSet candidateSet) { var items = new List <(int index, ActionSelectorCandidate candidate)>(); // We want to execute a group at a time (based on score) so keep track of the score that we've seen. int?score = null; // Perf: Avoid allocations for (var i = 0; i < candidateSet.Count; i++) { if (candidateSet.IsValidCandidate(i)) { ref var candidate = ref candidateSet[i]; if (score != null && score != candidate.Score) { // This is the end of a group. var matches = EvaluateActionConstraintsCore(httpContext, candidateSet, items, startingOrder: null); if (matches?.Count > 0) { return(matches); } // If we didn't find matches, then reset. items.Clear(); } score = candidate.Score; // If we get here, this is either the first endpoint or the we just (unsuccessfully) // executed constraints for a group. // // So keep adding constraints. var endpoint = candidate.Endpoint; var actionDescriptor = endpoint.Metadata.GetMetadata <ActionDescriptor>(); IReadOnlyList <IActionConstraint> constraints = Array.Empty <IActionConstraint>(); if (actionDescriptor != null) { constraints = _actionConstraintCache.GetActionConstraints(httpContext, actionDescriptor); } // Capture the index. We need this later to look up the endpoint/route values. items.Add((i, new ActionSelectorCandidate(actionDescriptor ?? NonAction, constraints))); } }
/// <inheritdoc/> public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { _ = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); _ = candidates ?? throw new ArgumentNullException(nameof(candidates)); var headers = httpContext.Request.Headers; for (var i = 0; i < candidates.Count; i++) { if (!candidates.IsValidCandidate(i)) { continue; } var matchers = candidates[i].Endpoint.Metadata.GetMetadata <IHeaderMetadata>()?.Matchers; if (matchers is null) { continue; } foreach (var matcher in matchers) { if (headers.TryGetValue(matcher.Name, out var requestHeaderValues) && !StringValues.IsNullOrEmpty(requestHeaderValues)) { if (matcher.Mode is HeaderMatchMode.Exists) { continue; } if (matcher.Mode is HeaderMatchMode.ExactHeader or HeaderMatchMode.HeaderPrefix ? TryMatchExactOrPrefix(matcher, requestHeaderValues) : TryMatchContainsOrNotContains(matcher, requestHeaderValues)) { continue; } } candidates.SetValidity(i, false); break; } } return(Task.CompletedTask); }
/// <inheritdoc/> public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { _ = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); _ = candidates ?? throw new ArgumentNullException(nameof(candidates)); var query = httpContext.Request.Query; for (var i = 0; i < candidates.Count; i++) { if (!candidates.IsValidCandidate(i)) { continue; } var matchers = candidates[i].Endpoint.Metadata.GetMetadata <IQueryParameterMetadata>()?.Matchers; if (matchers is null) { continue; } foreach (var matcher in matchers) { if (query.TryGetValue(matcher.Name, out var requestQueryParameterValues) && !StringValues.IsNullOrEmpty(requestQueryParameterValues)) { if (matcher.Mode is QueryParameterMatchMode.Exists) { continue; } if (TryMatch(matcher, requestQueryParameterValues)) { continue; } } candidates.SetValidity(i, false); break; } } return(Task.CompletedTask); }
static IReadOnlyList <(int index, ActionDescriptor action)> EvaluateApiVersion( HttpContext httpContext, CandidateSet candidates, ApiVersion apiVersion) { Contract.Requires(httpContext != null); Contract.Requires(candidates != null); Contract.Ensures(Contract.Result <IReadOnlyList <(int index, ActionDescriptor action)> >() != null); var bestMatches = new List <(int index, ActionDescriptor action)>(); var implicitMatches = new List <(int, ActionDescriptor)>(); for (var i = 0; i < candidates.Count; i++) { if (!candidates.IsValidCandidate(i)) { continue; } ref var candidate = ref candidates[i]; var action = candidate.Endpoint.Metadata?.GetMetadata <ActionDescriptor>(); if (action == null) { candidates.SetValidity(i, false); continue; } switch (action.MappingTo(apiVersion)) { case Explicit: bestMatches.Add((i, action)); break; case Implicit: implicitMatches.Add((i, action)); break; } // perf: always make the candidate invalid so we only need to loop through the // final, best matches for any remaining, valid candidates candidates.SetValidity(i, false); }
static (bool Matched, bool HasCandidates) MatchApiVersion(CandidateSet candidates, ApiVersion?apiVersion) { var bestMatches = new List <int>(); var implicitMatches = new List <int>(); var hasCandidates = false; for (var i = 0; i < candidates.Count; i++) { if (!candidates.IsValidCandidate(i)) { continue; } hasCandidates = true; ref var candidate = ref candidates[i]; var action = candidate.Endpoint.Metadata.GetMetadata <ActionDescriptor>(); if (action == null) { continue; } // remember whether the candidate is currently valid. a matching api version will not // make the candidate valid; however, we want to short-circuit with 400 if no candidates // match the api version at all. switch (action.MappingTo(apiVersion)) { case Explicit: bestMatches.Add(i); break; case Implicit: implicitMatches.Add(i); break; } // perf: always make the candidate invalid so we only need to loop through the // final, best matches for any remaining candidates candidates.SetValidity(i, false); }
public async Task ApplyAsync_HasMatchFindsEndpoint_WithoutRouteValues() { // Arrange var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer); var endpoints = new[] { DynamicEndpoint, }; var values = new RouteValueDictionary[] { null, }; var scores = new[] { 0, }; var candidates = new CandidateSet(endpoints, values, scores); Transform = (c, values) => { return(new ValueTask <RouteValueDictionary>(new RouteValueDictionary(new { page = "/Index", }))); }; var httpContext = new DefaultHttpContext() { RequestServices = Services, }; // Act await policy.ApplyAsync(httpContext, candidates); // Assert Assert.Same(LoadedEndpoint, candidates[0].Endpoint); Assert.Collection( candidates[0].Values.OrderBy(kvp => kvp.Key), kvp => { Assert.Equal("page", kvp.Key); Assert.Equal("/Index", kvp.Value); }); Assert.True(candidates.IsValidCandidate(0)); }
/// <summary> /// /// </summary> /// <param name="httpContext"></param> /// <param name="candidates"></param> /// <returns></returns> public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { // The goal of this method is to perform the final matching: // Map between route values matched by the template and the ones we want to expose to the action for binding. // (tweaking the route values is fine here) // Invalidating the candidate if the key/function values are not valid/missing. // Perform overload resolution for functions by looking at the candidates and their metadata. for (var i = 0; i < candidates.Count; i++) { ref var candidate = ref candidates[i]; if (!candidates.IsValidCandidate(i)) { continue; } var oDataMetadata = candidate.Endpoint.Metadata.OfType <ODataEndpointMetadata>().FirstOrDefault(); if (oDataMetadata == null) { continue; } var originalValues = candidate.Values; var oPath = oDataMetadata.GenerateODataPath(originalValues, httpContext.Request.QueryString); if (oPath != null) { var odata = httpContext.Request.ODataFeature(); odata.Model = oDataMetadata.Model; odata.Path = oPath; //candidates.SetValidity(i, true); // Double confirm whether it's required or not? continue; } else { candidates.SetValidity(i, false); continue; } }
public async Task ApplyAsync_CanDiscardFoundEndpoints() { // Arrange var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer); var endpoints = new[] { DynamicEndpoint, }; var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), }; var scores = new[] { 0, }; var candidates = new CandidateSet(endpoints, values, scores); Transform = (c, values, state) => { return(new ValueTask <RouteValueDictionary>(new RouteValueDictionary(new { controller = "Home", action = "Index", state }))); }; Filter = (c, values, state, endpoints) => { return(new ValueTask <IReadOnlyList <Endpoint> >(Array.Empty <Endpoint>())); }; var httpContext = new DefaultHttpContext() { RequestServices = Services, }; // Act await policy.ApplyAsync(httpContext, candidates); // Assert Assert.False(candidates.IsValidCandidate(0)); }
internal async Task ApplyAsyncInternal(HttpContext httpContext, CandidateSet candidates) { var featureManager = httpContext.RequestServices.GetRequiredService <IFeatureManagerSnapshot <TFeature> >(); for (var i = 0; i < candidates.Count; i++) { if (candidates.IsValidCandidate(i)) { var candidate = candidates[i]; var enabled = true; foreach (var metadata in candidate.Endpoint.Metadata .GetOrderedMetadata <IFeatureActionConstraintMetadata <TFeature> >() .Where(m => m.Features?.Any() == true)) { enabled = enabled && await featureManager.IsEnabledAsync(metadata.RequirementType, metadata.Features).ConfigureAwait(false); if (!enabled) { // If the endpoint has multiple feature action constraints we don't want to evaluate any // more constraints if we have already determined that the endpoint is not enabled. break; } } if (!enabled) { // The endpoint is not be enabled so set the endpoint validity to false. candidates.SetValidity(i, false); } } } return; }
public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { if (httpContext == null) { throw new ArgumentNullException(nameof(httpContext)); } IODataFeature odataFeature = httpContext.ODataFeature(); if (odataFeature.Path != null) { // If we have the OData path setting, it means there's some Policy working. // Let's skip this default OData matcher policy. return(Task.CompletedTask); } for (var i = 0; i < candidates.Count; i++) { ref CandidateState candidate = ref candidates[i]; if (!candidates.IsValidCandidate(i)) { continue; } IODataRoutingMetadata metadata = candidate.Endpoint.Metadata.OfType <IODataRoutingMetadata>().FirstOrDefault(); if (metadata == null) { continue; } // Get api-version query from HttpRequest? QueryStringApiVersionReader reader = new QueryStringApiVersionReader("api-version"); string apiVersionStr = reader.Read(httpContext.Request); if (apiVersionStr == null) { candidates.SetValidity(i, false); continue; } ApiVersion apiVersion = ApiVersion.Parse(apiVersionStr); IEdmModel model = GetEdmModel(apiVersion); if (model == null) { candidates.SetValidity(i, false); continue; } if (!IsApiVersionMatch(candidate.Endpoint.Metadata, apiVersion)) { candidates.SetValidity(i, false); continue; } ODataTemplateTranslateContext translatorContext = new ODataTemplateTranslateContext(httpContext, candidate.Endpoint, candidate.Values, model); try { ODataPath odataPath = _translator.Translate(metadata.Template, translatorContext); if (odataPath != null) { odataFeature.RoutePrefix = metadata.Prefix; odataFeature.Model = model; odataFeature.Path = odataPath; ODataOptions options = new ODataOptions(); UpdateQuerySetting(options); options.AddRouteComponents(model); odataFeature.Services = options.GetRouteServices(string.Empty); MergeRouteValues(translatorContext.UpdatedValues, candidate.Values); } else { candidates.SetValidity(i, false); } } catch { candidates.SetValidity(i, false); } }
/// <summary> /// Applies the policy to the CandidateSet. /// </summary> /// <param name="httpContext">The context associated with the current request.</param> /// <param name="candidates">The CandidateSet.</param> /// <returns>The task.</returns> public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { if (httpContext == null) { throw Error.ArgumentNull(nameof(httpContext)); } IODataFeature odataFeature = httpContext.ODataFeature(); if (odataFeature.Path != null) { // If we have the OData path setting, it means there's some Policy working. // Let's skip this default OData matcher policy. return(Task.CompletedTask); } // The goal of this method is to perform the final matching: // Map between route values matched by the template and the ones we want to expose to the action for binding. // (tweaking the route values is fine here) // Invalidating the candidate if the key/function values are not valid/missing. // Perform overload resolution for functions by looking at the candidates and their metadata. for (var i = 0; i < candidates.Count; i++) { ref CandidateState candidate = ref candidates[i]; if (!candidates.IsValidCandidate(i)) { continue; } IODataRoutingMetadata metadata = candidate.Endpoint.Metadata.OfType <IODataRoutingMetadata>().FirstOrDefault(); if (metadata == null) { continue; } if (odataFeature.Path != null) { // If it's odata endpoint, and we have a path set, let other odata endpoints invalid. candidates.SetValidity(i, false); continue; } ODataTemplateTranslateContext translatorContext = new ODataTemplateTranslateContext(httpContext, candidate.Endpoint, candidate.Values, metadata.Model); ODataPath odataPath = _translator.Translate(metadata.Template, translatorContext); if (odataPath != null) { odataFeature.RoutePrefix = metadata.Prefix; odataFeature.Model = metadata.Model; odataFeature.Path = odataPath; MergeRouteValues(translatorContext.UpdatedValues, candidate.Values); // Shall we break the remaining candidates? // So far the answer is no. Because we can use this matcher to obsolete the unmatched endpoint. // break; } else { candidates.SetValidity(i, false); } }
public async Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { if (httpContext == null) { throw new ArgumentNullException(nameof(httpContext)); } if (candidates == null) { throw new ArgumentNullException(nameof(candidates)); } // The per-route selector, must be the same for all the endpoints we are dealing with. DynamicControllerEndpointSelector?selector = null; // There's no real benefit here from trying to avoid the async state machine. // We only execute on nodes that contain a dynamic policy, and thus always have // to await something. for (var i = 0; i < candidates.Count; i++) { if (!candidates.IsValidCandidate(i)) { continue; } var endpoint = candidates[i].Endpoint; var originalValues = candidates[i].Values !; RouteValueDictionary?dynamicValues = null; // We don't expect both of these to be provided, and they are internal so there's // no realistic way this could happen. var dynamicControllerMetadata = endpoint.Metadata.GetMetadata <DynamicControllerMetadata>(); var transformerMetadata = endpoint.Metadata.GetMetadata <DynamicControllerRouteValueTransformerMetadata>(); DynamicRouteValueTransformer?transformer = null; if (dynamicControllerMetadata != null) { dynamicValues = dynamicControllerMetadata.Values; } else if (transformerMetadata != null) { transformer = (DynamicRouteValueTransformer)httpContext.RequestServices.GetRequiredService(transformerMetadata.SelectorType); if (transformer.State != null) { throw new InvalidOperationException(Resources.FormatStateShouldBeNullForRouteValueTransformers(transformerMetadata.SelectorType.Name)); } transformer.State = transformerMetadata.State; dynamicValues = await transformer.TransformAsync(httpContext, originalValues); } else { // Not a dynamic controller. continue; } if (dynamicValues == null) { candidates.ReplaceEndpoint(i, null, null); continue; } selector = ResolveSelector(selector, endpoint); var endpoints = selector.SelectEndpoints(dynamicValues); if (endpoints.Count == 0 && dynamicControllerMetadata != null) { // Naving no match for a fallback is a configuration error. We can't really check // during startup that the action you configured exists, so this is the best we can do. throw new InvalidOperationException( "Cannot find the fallback endpoint specified by route values: " + "{ " + string.Join(", ", dynamicValues.Select(kvp => $"{kvp.Key}: {kvp.Value}")) + " }."); } else if (endpoints.Count == 0) { candidates.ReplaceEndpoint(i, null, null); continue; } // We need to provide the route values associated with this endpoint, so that features // like URL generation work. var values = new RouteValueDictionary(dynamicValues); // Include values that were matched by the fallback route. if (originalValues != null) { foreach (var kvp in originalValues) { values.TryAdd(kvp.Key, kvp.Value); } } if (transformer != null) { endpoints = await transformer.FilterAsync(httpContext, values, endpoints); if (endpoints.Count == 0) { candidates.ReplaceEndpoint(i, null, null); continue; } } // Update the route values candidates.ReplaceEndpoint(i, endpoint, values); // Expand the list of endpoints candidates.ExpandEndpoint(i, endpoints, _comparer); } }
/// <inheritdoc /> public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { if (httpContext == null) { throw new ArgumentNullException(nameof(httpContext)); } if (candidates == null) { throw new ArgumentNullException(nameof(candidates)); } for (var i = 0; i < candidates.Count; i++) { if (!candidates.IsValidCandidate(i)) { continue; } var hosts = candidates[i].Endpoint.Metadata.GetMetadata <IHostMetadata>()?.Hosts; if (hosts == null || hosts.Count == 0) { // Can match any host. continue; } var matched = false; var(requestHost, requestPort) = GetHostAndPort(httpContext); for (var j = 0; j < hosts.Count; j++) { var host = hosts[j].AsSpan(); var port = ReadOnlySpan <char> .Empty; // Split into host and port var pivot = host.IndexOf(':'); if (pivot >= 0) { port = host.Slice(pivot + 1); host = host.Slice(0, pivot); } if (host == null || MemoryExtensions.Equals(host, WildcardHost, StringComparison.OrdinalIgnoreCase)) { // Can match any host } else if ( host.StartsWith(WildcardPrefix) && // Note that we only slice off the `*`. We want to match the leading `.` also. MemoryExtensions.EndsWith(requestHost, host.Slice(WildcardHost.Length), StringComparison.OrdinalIgnoreCase)) { // Matches a suffix wildcard. } else if (MemoryExtensions.Equals(requestHost, host, StringComparison.OrdinalIgnoreCase)) { // Matches exactly } else { // If we get here then the host doesn't match. continue; } if (MemoryExtensions.Equals(port, WildcardHost, StringComparison.OrdinalIgnoreCase)) { // Port is a wildcard, we allow any port. } else if (port.Length > 0 && (!int.TryParse(port, out var parsed) || parsed != requestPort)) { // If we get here then the port doesn't match. continue; } matched = true; break; } if (!matched) { candidates.SetValidity(i, false); } } return(Task.CompletedTask); }
public async Task ApplyAsync(HttpContext httpContext, EndpointSelectorContext context, CandidateSet candidates) { if (httpContext == null) { throw new ArgumentNullException(nameof(httpContext)); } if (context == null) { throw new ArgumentNullException(nameof(context)); } if (candidates == null) { throw new ArgumentNullException(nameof(candidates)); } // There's no real benefit here from trying to avoid the async state machine. // We only execute on nodes that contain a dynamic policy, and thus always have // to await something. for (var i = 0; i < candidates.Count; i++) { if (!candidates.IsValidCandidate(i)) { continue; } var endpoint = candidates[i].Endpoint; var metadata = endpoint.Metadata.GetMetadata <DynamicPageMetadata>(); if (metadata == null) { continue; } var matchedValues = candidates[i].Values; var endpoints = _selector.SelectEndpoints(metadata.Values); if (endpoints.Count == 0) { // If there's no match this is a configuration error. We can't really check // during startup that the action you configured exists. throw new InvalidOperationException( "Cannot find the fallback endpoint specified by route values: " + "{ " + string.Join(", ", metadata.Values.Select(kvp => $"{kvp.Key}: {kvp.Value}")) + " }."); } var compiled = await _loader.LoadAsync(endpoints[0].Metadata.GetMetadata <PageActionDescriptor>()); var replacement = compiled.Endpoint; // We need to provide the route values associated with this endpoint, so that features // like URL generation work. var values = new RouteValueDictionary(metadata.Values); // Include values that were matched by the fallback route. foreach (var kvp in matchedValues) { values.TryAdd(kvp.Key, kvp.Value); } candidates.ReplaceEndpoint(i, replacement, values); } }
/// <summary> /// For framework use only. /// </summary> /// <param name="httpContext"></param> /// <param name="candidates"></param> /// <returns></returns> public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { if (httpContext == null) { throw new ArgumentNullException(nameof(httpContext)); } if (candidates == null) { throw new ArgumentNullException(nameof(candidates)); } // Returning a 405 here requires us to return keep track of all 'seen' HTTP methods. We allocate to // keep track of this because we either need to keep track of the HTTP methods or keep track of the // endpoints - both allocate. // // Those code only runs in the presence of dynamic endpoints anyway. // // We want to return a 405 iff we eliminated ALL of the currently valid endpoints due to HTTP method // mismatch. bool? needs405Endpoint = null; HashSet <string>?methods = null; for (var i = 0; i < candidates.Count; i++) { // We do this check first for consistency with how 405 is implemented for the graph version // of this code. We still want to know if any endpoints in this set require an HTTP method // even if those endpoints are already invalid - hence the null-check. var metadata = candidates[i].Endpoint?.Metadata.GetMetadata <IHttpMethodMetadata>(); if (metadata == null || metadata.HttpMethods.Count == 0) { // Can match any method. needs405Endpoint = false; continue; } // Saw a valid endpoint. needs405Endpoint = needs405Endpoint ?? true; if (!candidates.IsValidCandidate(i)) { continue; } var httpMethod = httpContext.Request.Method; var headers = httpContext.Request.Headers; if (metadata.AcceptCorsPreflight && HttpMethods.Equals(httpMethod, PreflightHttpMethod) && headers.ContainsKey(HeaderNames.Origin) && headers.TryGetValue(HeaderNames.AccessControlRequestMethod, out var accessControlRequestMethod) && !StringValues.IsNullOrEmpty(accessControlRequestMethod)) { needs405Endpoint = false; // We don't return a 405 for a CORS preflight request when the endpoints accept CORS preflight. httpMethod = accessControlRequestMethod.ToString(); } var matched = false; for (var j = 0; j < metadata.HttpMethods.Count; j++) { var candidateMethod = metadata.HttpMethods[j]; if (!HttpMethods.Equals(httpMethod, candidateMethod)) { methods = methods ?? new HashSet <string>(StringComparer.OrdinalIgnoreCase); methods.Add(candidateMethod); continue; } matched = true; needs405Endpoint = false; break; } if (!matched) { candidates.SetValidity(i, false); } } if (needs405Endpoint == true) { // We saw some endpoints coming in, and we eliminated them all. httpContext.SetEndpoint(CreateRejectionEndpoint(methods !.OrderBy(m => m, StringComparer.OrdinalIgnoreCase))); httpContext.Request.RouteValues = null !; } return(Task.CompletedTask); }
public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { if (httpContext == null) { throw new ArgumentNullException(nameof(httpContext)); } // The goal of this method is to perform the final matching: // Map between route values matched by the template and the ones we want to expose to the action for binding. // (tweaking the route values is fine here) // Invalidating the candidate if the key/function values are not valid/missing. // Perform overload resolution for functions by looking at the candidates and their metadata. for (var i = 0; i < candidates.Count; i++) { ref CandidateState candidate = ref candidates[i]; if (!candidates.IsValidCandidate(i)) { continue; } IODataRoutingMetadata metadata = candidate.Endpoint.Metadata.OfType <IODataRoutingMetadata>().FirstOrDefault(); if (metadata == null) { continue; } IHttpMethodMetadata httpMetadata = candidate.Endpoint.Metadata.GetMetadata <IHttpMethodMetadata>(); if (httpMetadata == null) { // Check the http method if (metadata.HttpMethods != null && !metadata.HttpMethods.Contains(httpContext.Request.Method)) { candidates.SetValidity(i, false); continue; } } ODataTemplateTranslateContext translatorContext = new ODataTemplateTranslateContext(httpContext, candidate.Values, metadata.Model); try { ODataPath odataPath = _translator.Translate(metadata.Template, translatorContext); if (odataPath != null) { IODataFeature odataFeature = httpContext.ODataFeature(); odataFeature.PrefixName = metadata.Prefix; odataFeature.Model = metadata.Model; odataFeature.Path = odataPath; MergeRouteValues(translatorContext.UpdatedValues, candidate.Values); } else { candidates.SetValidity(i, false); } } #if DEBUG catch (Exception ex) { Console.WriteLine(ex.Message); #else catch (Exception) { #endif candidates.SetValidity(i, false); } }
/// <inheritdoc/> public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { _ = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); _ = candidates ?? throw new ArgumentNullException(nameof(candidates)); for (var i = 0; i < candidates.Count; i++) { if (!candidates.IsValidCandidate(i)) { continue; } var matchers = candidates[i].Endpoint.Metadata.GetMetadata <IHeaderMetadata>()?.Matchers; if (matchers == null) { continue; } for (var m = 0; m < matchers.Count; m++) { var matcher = matchers[m]; var expectedHeaderName = matcher.Name; var expectedHeaderValues = matcher.Values; var matched = false; if (httpContext.Request.Headers.TryGetValue(expectedHeaderName, out var requestHeaderValues)) { if (StringValues.IsNullOrEmpty(requestHeaderValues)) { // A non-empty value is required for a match. } else if (matcher.Mode == HeaderMatchMode.Exists) { // We were asked to match as long as the header exists, and it *does* exist matched = true; } // Multi-value headers are not supported. // Note a single entry may also contain multiple values, we don't distinguish, we only match on the whole header. else if (requestHeaderValues.Count == 1) { var requestHeaderValue = requestHeaderValues.ToString(); for (var j = 0; j < expectedHeaderValues.Count; j++) { if (MatchHeader(matcher.Mode, requestHeaderValue, expectedHeaderValues[j], matcher.IsCaseSensitive)) { matched = true; break; } } } } // All rules must match if (!matched) { candidates.SetValidity(i, false); break; } } } return(Task.CompletedTask); }
/// <summary> /// /// </summary> /// <param name="httpContext"></param> /// <param name="candidates"></param> /// <returns></returns> public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { // The goal of this method is to perform the final matching: // Map between route values matched by the template and the ones we want to expose to the action for binding. // (tweaking the route values is fine here) // Invalidating the candidate if the key/function values are not valid/missing. // Perform overload resolution for functions by looking at the candidates and their metadata. for (var i = 0; i < candidates.Count; i++) { ref var candidate = ref candidates[i]; if (!candidates.IsValidCandidate(i)) { continue; } var oDataMetadata = candidate.Endpoint.Metadata.OfType <ODataEndpointMetadata>().FirstOrDefault(); if (oDataMetadata == null) { continue; } var original = candidate.Endpoint.RequestDelegate; var name = candidate.Endpoint.DisplayName; var newEndpoint = new Endpoint(EndpointWithODataPath, candidate.Endpoint.Metadata, name); var originalValues = candidate.Values; var newValues = new RouteValueDictionary(); foreach (var(key, value) in originalValues) { //if (key.EndsWith(".Name")) //{ // var keyValue = originalValues[key.Replace(".Name", ".Value")]; // var partName = originalValues[key]; // var parameterName = oDataMetadata.ParameterMappings[oDataMetadata.ParameterMappings.Keys.Single(key => key.Name == (string)partName)]; // newValues.Add(parameterName, keyValue); //} newValues.Add(key, value); } var oPath = oDataMetadata.GenerateODataPath(originalValues, httpContext.Request.QueryString); if (oPath != null) { var odata = httpContext.Request.ODataFeature(); odata.Model = oDataMetadata.Model; odata.IsEndpointRouting = true; odata.RequestContainer = httpContext.RequestServices; // sp; odata.Path = oPath; //candidates.SetValidity(i, true); // Double confirm whether it's required or not? continue; } else { candidates.SetValidity(i, false); continue; } //candidates.ReplaceEndpoint(i, newEndpoint, newValues); Task EndpointWithODataPath(HttpContext httpContext) { var odataPath = oDataMetadata.ODataPathFactory(httpContext.GetRouteData().Values, oDataMetadata.ParameterMappings); var odata = httpContext.Request.ODataFeature(); odata.IsEndpointRouting = true; odata.RequestContainer = httpContext.RequestServices; odata.Path = odataPath; odata.RouteName = name; var prc = httpContext.RequestServices.GetRequiredService <IPerRouteContainer>(); if (!prc.HasODataRootContainer(name)) { prc.AddRoute(odata.RouteName, ""); } return(original(httpContext)); } }