// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Microsoft.AspNet.Routing.Template { public class TemplateRoute : INamedRouter { private readonly IReadOnlyDictionary _constraints; private readonly IReadOnlyDictionary _dataTokens; private readonly IReadOnlyDictionary _defaults; private readonly IRouter _target; private readonly RouteTemplate _parsedTemplate; private readonly string _routeTemplate; private readonly TemplateMatcher _matcher; private readonly TemplateBinder _binder; private ILogger _logger; private ILogger _constraintLogger; public TemplateRoute( IRouter target, string routeTemplate, IInlineConstraintResolver inlineConstraintResolver) : this( target, routeTemplate, defaults: null, constraints: null, dataTokens: null, inlineConstraintResolver: inlineConstraintResolver) { } public TemplateRoute( IRouter target, string routeTemplate, IDictionary defaults, IDictionary constraints, IDictionary dataTokens, IInlineConstraintResolver inlineConstraintResolver) : this(target, null, routeTemplate, defaults, constraints, dataTokens, inlineConstraintResolver) { } public TemplateRoute( IRouter target, string routeName, string routeTemplate, IDictionary defaults, IDictionary constraints, IDictionary dataTokens, IInlineConstraintResolver inlineConstraintResolver) { if (target == null) { throw new ArgumentNullException(nameof(target)); } _target = target; _routeTemplate = routeTemplate ?? string.Empty; Name = routeName; _dataTokens = dataTokens == null ? RouteValueDictionary.Empty : new RouteValueDictionary(dataTokens); // Data we parse from the template will be used to fill in the rest of the constraints or // defaults. The parser will throw for invalid routes. _parsedTemplate = TemplateParser.Parse(RouteTemplate); _constraints = GetConstraints(inlineConstraintResolver, RouteTemplate, _parsedTemplate, constraints); _defaults = GetDefaults(_parsedTemplate, defaults); _matcher = new TemplateMatcher(_parsedTemplate, Defaults); _binder = new TemplateBinder(_parsedTemplate, Defaults); } public string Name { get; private set; } public IReadOnlyDictionary Defaults { get { return _defaults; } } public IReadOnlyDictionary DataTokens { get { return _dataTokens; } } public RouteTemplate ParsedTemplate { get { return _parsedTemplate; } } public string RouteTemplate { get { return _routeTemplate; } } public IReadOnlyDictionary Constraints { get { return _constraints; } } public async virtual Task RouteAsync(RouteContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } EnsureLoggers(context.HttpContext); var requestPath = context.HttpContext.Request.Path; var values = _matcher.Match(requestPath); if (values == null) { // If we got back a null value set, that means the URI did not match return; } var oldRouteData = context.RouteData; var newRouteData = new RouteData(oldRouteData); // Perf: Avoid accessing data tokens if you don't need to write to it, these dictionaries are all // created lazily. if (_dataTokens.Count > 0) { MergeValues(newRouteData.DataTokens, _dataTokens); } newRouteData.Routers.Add(_target); MergeValues(newRouteData.Values, values); if (!RouteConstraintMatcher.Match( Constraints, newRouteData.Values, context.HttpContext, this, RouteDirection.IncomingRequest, _constraintLogger)) { return; } _logger.LogVerbose( "Request successfully matched the route with name '{RouteName}' and template '{RouteTemplate}'.", Name, RouteTemplate); try { context.RouteData = newRouteData; await _target.RouteAsync(context); } finally { // Restore the original values to prevent polluting the route data. if (!context.IsHandled) { context.RouteData = oldRouteData; } } } public virtual VirtualPathData GetVirtualPath(VirtualPathContext context) { var values = _binder.GetValues(context.AmbientValues, context.Values); if (values == null) { // We're missing one of the required values for this route. return null; } EnsureLoggers(context.Context); if (!RouteConstraintMatcher.Match(Constraints, values.CombinedValues, context.Context, this, RouteDirection.UrlGeneration, _constraintLogger)) { return null; } // Validate that the target can accept these values. var childContext = CreateChildVirtualPathContext(context, values.AcceptedValues); var pathData = _target.GetVirtualPath(childContext); if (pathData != null) { // If the target generates a value then that can short circuit. return pathData; } // If we can produce a value go ahead and do it, the caller can check context.IsBound // to see if the values were validated. // When we still cannot produce a value, this should return null. var tempPath = _binder.BindValues(values.AcceptedValues); if (tempPath == null) { return null; } pathData = new VirtualPathData(this, tempPath); if (DataTokens != null) { foreach (var dataToken in DataTokens) { pathData.DataTokens.Add(dataToken.Key, dataToken.Value); } } context.IsBound = childContext.IsBound; return pathData; } private VirtualPathContext CreateChildVirtualPathContext( VirtualPathContext context, IDictionary acceptedValues) { // We want to build the set of values that would be provided if this route were to generated // a link and then immediately match it. This includes all the accepted parameter values, and // the defaults. Accepted values that would go in the query string aren't included. var providedValues = new RouteValueDictionary(); foreach (var parameter in _parsedTemplate.Parameters) { object value; if (acceptedValues.TryGetValue(parameter.Name, out value)) { providedValues.Add(parameter.Name, value); } } foreach (var kvp in _defaults) { if (!providedValues.ContainsKey(kvp.Key)) { providedValues.Add(kvp.Key, kvp.Value); } } return new VirtualPathContext(context.Context, context.AmbientValues, context.Values) { ProvidedValues = providedValues, }; } private static IReadOnlyDictionary GetConstraints( IInlineConstraintResolver inlineConstraintResolver, string template, RouteTemplate parsedTemplate, IDictionary constraints) { var constraintBuilder = new RouteConstraintBuilder(inlineConstraintResolver, template); if (constraints != null) { foreach (var kvp in constraints) { constraintBuilder.AddConstraint(kvp.Key, kvp.Value); } } foreach (var parameter in parsedTemplate.Parameters) { if (parameter.IsOptional) { constraintBuilder.SetOptional(parameter.Name); } foreach (var inlineConstraint in parameter.InlineConstraints) { constraintBuilder.AddResolvedConstraint(parameter.Name, inlineConstraint.Constraint); } } return constraintBuilder.Build(); } private static RouteValueDictionary GetDefaults( RouteTemplate parsedTemplate, IDictionary defaults) { // Do not use RouteValueDictionary.Empty for defaults, it might be modified inside // UpdateInlineDefaultValuesAndConstraints() var result = defaults == null ? new RouteValueDictionary() : new RouteValueDictionary(defaults); foreach (var parameter in parsedTemplate.Parameters) { if (parameter.DefaultValue != null) { if (result.ContainsKey(parameter.Name)) { throw new InvalidOperationException( Resources.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly( parameter.Name)); } else { result.Add(parameter.Name, parameter.DefaultValue); } } } return result; } private static void MergeValues( RouteValueDictionary destination, RouteValueDictionary values) { foreach (var kvp in values) { // This will replace the original value for the specified key. // Values from the matched route will take preference over previous // data in the route context. destination[kvp.Key] = kvp.Value; } } // Needed because IDictionary<> is not an IReadOnlyDictionary<> private static void MergeValues( IDictionary destination, IReadOnlyDictionary values) { foreach (var kvp in values) { // This will replace the original value for the specified key. // Values from the matched route will take preference over previous // data in the route context. destination[kvp.Key] = kvp.Value; } } private void EnsureLoggers(HttpContext context) { if (_logger == null) { var factory = context.RequestServices.GetRequiredService(); _logger = factory.CreateLogger(); _constraintLogger = factory.CreateLogger(typeof(RouteConstraintMatcher).FullName); } } public override string ToString() { return _routeTemplate; } } }