asp.net core mvc 3.1 源码分析(一)
我们先看下IApplicationBuilder接口的扩展方法UseMvc
public static IApplicationBuilder UseMvc( this IApplicationBuilder app, Action<IRouteBuilder> configureRoutes) { if (app == null) { throw new ArgumentNullException(nameof(app)); } if (configureRoutes == null) { throw new ArgumentNullException(nameof(configureRoutes)); } VerifyMvcIsRegistered(app); var options = app.ApplicationServices.GetRequiredService<IOptions<MvcOptions>>(); if (options.Value.EnableEndpointRouting) { var message = "Endpoint Routing does not support 'IApplicationBuilder.UseMvc(...)'. To use " + "'IApplicationBuilder.UseMvc' set 'MvcOptions.EnableEndpointRouting = false' inside " + "'ConfigureServices(...)."; throw new InvalidOperationException(message); } var routes = new RouteBuilder(app) { DefaultHandler = app.ApplicationServices.GetRequiredService<MvcRouteHandler>(), }; configureRoutes(routes); routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(app.ApplicationServices)); return app.UseRouter(routes.Build()); }
先判断是否注册了Mvc的相关服务
再判断MvcOptions的属性EnableEndpointRouting,如果要用UseMvc,则需设置该属性为False
接着创建RouteBuilder对象,其DefaultHandler属性为MvcRouteHandler,MvcRouteHandler会进入到mvc的处理流程中
在RouteBuilder的Routers属性插入AttributeRoute,用来处理特性路由,其对应的Handler为MvcAttributeRouteHandler
当我们注册路由后,默认会由MvcRouteHandler和MvcAttributeRouteHandler这两个Hander来出路mvc逻辑
我们看下平时是怎么注册路由的
public static IRouteBuilder MapRoute( this IRouteBuilder routeBuilder, string name, string template, object defaults, object constraints, object dataTokens) { if (routeBuilder.DefaultHandler == null) { throw new RouteCreationException(Resources.FormatDefaultHandler_MustBeSet(nameof(IRouteBuilder))); } routeBuilder.Routes.Add(new Route( routeBuilder.DefaultHandler, name, template, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints), new RouteValueDictionary(dataTokens), CreateInlineConstraintResolver(routeBuilder.ServiceProvider))); return routeBuilder; }
我们注册的route默认由IRouteBuilder的DefaultHandler来处理
MvcRouteHandler
internal class MvcRouteHandler : IRouter { private readonly IActionInvokerFactory _actionInvokerFactory; private readonly IActionSelector _actionSelector; private readonly ILogger _logger; private readonly DiagnosticListener _diagnosticListener; public MvcRouteHandler( IActionInvokerFactory actionInvokerFactory, IActionSelector actionSelector, DiagnosticListener diagnosticListener, ILoggerFactory loggerFactory) { _actionInvokerFactory = actionInvokerFactory; _actionSelector = actionSelector; _diagnosticListener = diagnosticListener; _logger = loggerFactory.CreateLogger<MvcRouteHandler>(); } public VirtualPathData GetVirtualPath(VirtualPathContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } // We return null here because we're not responsible for generating the url, the route is. return null; } public Task RouteAsync(RouteContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } var candidates = _actionSelector.SelectCandidates(context); if (candidates == null || candidates.Count == 0) { _logger.NoActionsMatched(context.RouteData.Values); return Task.CompletedTask; } var actionDescriptor = _actionSelector.SelectBestCandidate(context, candidates); if (actionDescriptor == null) { _logger.NoActionsMatched(context.RouteData.Values); return Task.CompletedTask; } context.Handler = (c) => { var routeData = c.GetRouteData(); var actionContext = new ActionContext(context.HttpContext, routeData, actionDescriptor); var invoker = _actionInvokerFactory.CreateInvoker(actionContext); if (invoker == null) { throw new InvalidOperationException( Resources.FormatActionInvokerFactory_CouldNotCreateInvoker( actionDescriptor.DisplayName)); } return invoker.InvokeAsync(); }; return Task.CompletedTask; } }
MvcRouteHandler通过IActionSelector找到合适ActionDescriptor,即找到对应Controller的Action方法
然后调用IActionInvokerFactory的CreateInvoker构建IActionInvoker对象,
最后调用IActionInvoker对象的InvokeAsync执行Action
MvcAttributeRouteHandler
internal class MvcAttributeRouteHandler : IRouter { private readonly IActionInvokerFactory _actionInvokerFactory; private readonly IActionSelector _actionSelector; private readonly ILogger _logger; private readonly DiagnosticListener _diagnosticListener; public MvcAttributeRouteHandler( IActionInvokerFactory actionInvokerFactory, IActionSelector actionSelector, DiagnosticListener diagnosticListener, ILoggerFactory loggerFactory) { _actionInvokerFactory = actionInvokerFactory; _actionSelector = actionSelector; _diagnosticListener = diagnosticListener; _logger = loggerFactory.CreateLogger<MvcAttributeRouteHandler>(); } public ActionDescriptor[] Actions { get; set; } public VirtualPathData GetVirtualPath(VirtualPathContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } // We return null here because we're not responsible for generating the url, the route is. return null; } public Task RouteAsync(RouteContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (Actions == null) { var message = Resources.FormatPropertyOfTypeCannotBeNull( nameof(Actions), nameof(MvcAttributeRouteHandler)); throw new InvalidOperationException(message); } var actionDescriptor = _actionSelector.SelectBestCandidate(context, Actions); if (actionDescriptor == null) { _logger.NoActionsMatched(context.RouteData.Values); return Task.CompletedTask; } foreach (var kvp in actionDescriptor.RouteValues) { if (!string.IsNullOrEmpty(kvp.Value)) { context.RouteData.Values[kvp.Key] = kvp.Value; } } context.Handler = (c) => { var routeData = c.GetRouteData(); var actionContext = new ActionContext(context.HttpContext, routeData, actionDescriptor); var invoker = _actionInvokerFactory.CreateInvoker(actionContext); if (invoker == null) { throw new InvalidOperationException( Resources.FormatActionInvokerFactory_CouldNotCreateInvoker( actionDescriptor.DisplayName)); } return invoker.InvokeAsync(); }; return Task.CompletedTask; } }
MvcAttributeRouteHandler和MvcRouteHandler处理逻辑差不多一样
主要看下AttributeRoute类如何处理特性路由
internal class AttributeRoute : IRouter { private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; private readonly IServiceProvider _services; private readonly Func<ActionDescriptor[], IRouter> _handlerFactory; private TreeRouter _router; public AttributeRoute( IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, IServiceProvider services, Func<ActionDescriptor[], IRouter> handlerFactory) { if (actionDescriptorCollectionProvider == null) { throw new ArgumentNullException(nameof(actionDescriptorCollectionProvider)); } if (services == null) { throw new ArgumentNullException(nameof(services)); } if (handlerFactory == null) { throw new ArgumentNullException(nameof(handlerFactory)); } _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; _services = services; _handlerFactory = handlerFactory; } /// <inheritdoc /> public VirtualPathData GetVirtualPath(VirtualPathContext context) { var router = GetTreeRouter(); return router.GetVirtualPath(context); } /// <inheritdoc /> public Task RouteAsync(RouteContext context) { var router = GetTreeRouter(); return router.RouteAsync(context); } private TreeRouter GetTreeRouter() { var actions = _actionDescriptorCollectionProvider.ActionDescriptors; // This is a safe-race. We'll never set router back to null after initializing // it on startup. if (_router == null || _router.Version != actions.Version) { var builder = _services.GetRequiredService<TreeRouteBuilder>(); AddEntries(builder, actions); _router = builder.Build(actions.Version); } return _router; } // internal for testing internal void AddEntries(TreeRouteBuilder builder, ActionDescriptorCollection actions) { var routeInfos = GetRouteInfos(actions.Items); // We're creating one TreeRouteLinkGenerationEntry per action. This allows us to match the intended // action by expected route values, and then use the TemplateBinder to generate the link. foreach (var routeInfo in routeInfos) { if (routeInfo.SuppressLinkGeneration) { continue; } var defaults = new RouteValueDictionary(); foreach (var kvp in routeInfo.ActionDescriptor.RouteValues) { defaults.Add(kvp.Key, kvp.Value); } try { // We use the `NullRouter` as the route handler because we don't need to do anything for link // generations. The TreeRouter does it all for us. builder.MapOutbound( NullRouter.Instance, routeInfo.RouteTemplate, defaults, routeInfo.RouteName, routeInfo.Order); } catch (RouteCreationException routeCreationException) { throw new RouteCreationException( "An error occurred while adding a route to the route builder. " + $"Route name '{routeInfo.RouteName}' and template '{routeInfo.RouteTemplate.TemplateText}'.", routeCreationException); } } // We're creating one AttributeRouteMatchingEntry per group, so we need to identify the distinct set of // groups. It's guaranteed that all members of the group have the same template and precedence, // so we only need to hang on to a single instance of the RouteInfo for each group. var groups = GetInboundRouteGroups(routeInfos); foreach (var group in groups) { var handler = _handlerFactory(group.ToArray()); // Note that because we only support 'inline' defaults, each routeInfo group also has the same // set of defaults. // // We then inject the route group as a default for the matcher so it gets passed back to MVC // for use in action selection. builder.MapInbound( handler, group.Key.RouteTemplate, group.Key.RouteName, group.Key.Order); } } private static IEnumerable<IGrouping<RouteInfo, ActionDescriptor>> GetInboundRouteGroups(List<RouteInfo> routeInfos) { return routeInfos .Where(routeInfo => !routeInfo.SuppressPathMatching) .GroupBy(r => r, r => r.ActionDescriptor, RouteInfoEqualityComparer.Instance); } private static List<RouteInfo> GetRouteInfos(IReadOnlyList<ActionDescriptor> actions) { var routeInfos = new List<RouteInfo>(); var errors = new List<RouteInfo>(); // This keeps a cache of 'Template' objects. It's a fairly common case that multiple actions // will use the same route template string; thus, the `Template` object can be shared. // // For a relatively simple route template, the `Template` object will hold about 500 bytes // of memory, so sharing is worthwhile. var templateCache = new Dictionary<string, RouteTemplate>(StringComparer.OrdinalIgnoreCase); var attributeRoutedActions = actions.Where(a => a.AttributeRouteInfo?.Template != null); foreach (var action in attributeRoutedActions) { var routeInfo = GetRouteInfo(templateCache, action); if (routeInfo.ErrorMessage == null) { routeInfos.Add(routeInfo); } else { errors.Add(routeInfo); } } if (errors.Count > 0) { var allErrors = string.Join( Environment.NewLine + Environment.NewLine, errors.Select( e => Resources.FormatAttributeRoute_IndividualErrorMessage( e.ActionDescriptor.DisplayName, Environment.NewLine, e.ErrorMessage))); var message = Resources.FormatAttributeRoute_AggregateErrorMessage(Environment.NewLine, allErrors); throw new RouteCreationException(message); } return routeInfos; } private static RouteInfo GetRouteInfo( Dictionary<string, RouteTemplate> templateCache, ActionDescriptor action) { var routeInfo = new RouteInfo() { ActionDescriptor = action, }; try { if (!templateCache.TryGetValue(action.AttributeRouteInfo.Template, out var parsedTemplate)) { // Parsing with throw if the template is invalid. parsedTemplate = TemplateParser.Parse(action.AttributeRouteInfo.Template); templateCache.Add(action.AttributeRouteInfo.Template, parsedTemplate); } routeInfo.RouteTemplate = parsedTemplate; routeInfo.SuppressPathMatching = action.AttributeRouteInfo.SuppressPathMatching; routeInfo.SuppressLinkGeneration = action.AttributeRouteInfo.SuppressLinkGeneration; } catch (Exception ex) { routeInfo.ErrorMessage = ex.Message; return routeInfo; } foreach (var kvp in action.RouteValues) { foreach (var parameter in routeInfo.RouteTemplate.Parameters) { if (string.Equals(kvp.Key, parameter.Name, StringComparison.OrdinalIgnoreCase)) { routeInfo.ErrorMessage = Resources.FormatAttributeRoute_CannotContainParameter( routeInfo.RouteTemplate.TemplateText, kvp.Key, kvp.Value); return routeInfo; } } } routeInfo.Order = action.AttributeRouteInfo.Order; routeInfo.RouteName = action.AttributeRouteInfo.Name; return routeInfo; } private class RouteInfo { public ActionDescriptor ActionDescriptor { get; set; } public string ErrorMessage { get; set; } public int Order { get; set; } public string RouteName { get; set; } public RouteTemplate RouteTemplate { get; set; } public bool SuppressPathMatching { get; set; } public bool SuppressLinkGeneration { get; set; } } private class RouteInfoEqualityComparer : IEqualityComparer<RouteInfo> { public static readonly RouteInfoEqualityComparer Instance = new RouteInfoEqualityComparer(); public bool Equals(RouteInfo x, RouteInfo y) { if (x == null && y == null) { return true; } else if (x == null ^ y == null) { return false; } else if (x.Order != y.Order) { return false; } else { return string.Equals( x.RouteTemplate.TemplateText, y.RouteTemplate.TemplateText, StringComparison.OrdinalIgnoreCase); } } public int GetHashCode(RouteInfo obj) { if (obj == null) { return 0; } var hash = new HashCodeCombiner(); hash.Add(obj.Order); hash.Add(obj.RouteTemplate.TemplateText, StringComparer.OrdinalIgnoreCase); return hash; } } // Used only to hook up link generation, and it doesn't need to do anything. private class NullRouter : IRouter { public static readonly NullRouter Instance = new NullRouter(); public VirtualPathData GetVirtualPath(VirtualPathContext context) { return null; } public Task RouteAsync(RouteContext context) { throw new NotImplementedException(); } } }
首先创建TreeRouteBuilder对象,再调用其Build方法创建TreeRouter
public class TreeRouteBuilder { private readonly ILogger _logger; private readonly ILogger _constraintLogger; private readonly UrlEncoder _urlEncoder; private readonly ObjectPool<UriBuildingContext> _objectPool; private readonly IInlineConstraintResolver _constraintResolver; /// <summary> /// Initializes a new instance of <see cref="TreeRouteBuilder"/>. /// </summary> /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param> /// <param name="objectPool">The <see cref="ObjectPool{UrlBuildingContext}"/>.</param> /// <param name="constraintResolver">The <see cref="IInlineConstraintResolver"/>.</param> internal TreeRouteBuilder( ILoggerFactory loggerFactory, ObjectPool<UriBuildingContext> objectPool, IInlineConstraintResolver constraintResolver) { if (loggerFactory == null) { throw new ArgumentNullException(nameof(loggerFactory)); } if (objectPool == null) { throw new ArgumentNullException(nameof(objectPool)); } if (constraintResolver == null) { throw new ArgumentNullException(nameof(constraintResolver)); } _urlEncoder = UrlEncoder.Default; _objectPool = objectPool; _constraintResolver = constraintResolver; _logger = loggerFactory.CreateLogger<TreeRouter>(); _constraintLogger = loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName); } /// <summary> /// Adds a new inbound route to the <see cref="TreeRouter"/>. /// </summary> /// <param name="handler">The <see cref="IRouter"/> for handling the route.</param> /// <param name="routeTemplate">The <see cref="RouteTemplate"/> of the route.</param> /// <param name="routeName">The route name.</param> /// <param name="order">The route order.</param> /// <returns>The <see cref="InboundRouteEntry"/>.</returns> public InboundRouteEntry MapInbound( IRouter handler, RouteTemplate routeTemplate, string routeName, int order) { if (handler == null) { throw new ArgumentNullException(nameof(handler)); } if (routeTemplate == null) { throw new ArgumentNullException(nameof(routeTemplate)); } var entry = new InboundRouteEntry() { Handler = handler, Order = order, Precedence = RoutePrecedence.ComputeInbound(routeTemplate), RouteName = routeName, RouteTemplate = routeTemplate, }; var constraintBuilder = new RouteConstraintBuilder(_constraintResolver, routeTemplate.TemplateText); foreach (var parameter in routeTemplate.Parameters) { if (parameter.InlineConstraints != null) { if (parameter.IsOptional) { constraintBuilder.SetOptional(parameter.Name); } foreach (var constraint in parameter.InlineConstraints) { constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint); } } } entry.Constraints = constraintBuilder.Build(); entry.Defaults = new RouteValueDictionary(); foreach (var parameter in entry.RouteTemplate.Parameters) { if (parameter.DefaultValue != null) { entry.Defaults.Add(parameter.Name, parameter.DefaultValue); } } InboundEntries.Add(entry); return entry; } /// <summary> /// Adds a new outbound route to the <see cref="TreeRouter"/>. /// </summary> /// <param name="handler">The <see cref="IRouter"/> for handling the link generation.</param> /// <param name="routeTemplate">The <see cref="RouteTemplate"/> of the route.</param> /// <param name="requiredLinkValues">The <see cref="RouteValueDictionary"/> containing the route values.</param> /// <param name="routeName">The route name.</param> /// <param name="order">The route order.</param> /// <returns>The <see cref="OutboundRouteEntry"/>.</returns> public OutboundRouteEntry MapOutbound( IRouter handler, RouteTemplate routeTemplate, RouteValueDictionary requiredLinkValues, string routeName, int order) { if (handler == null) { throw new ArgumentNullException(nameof(handler)); } if (routeTemplate == null) { throw new ArgumentNullException(nameof(routeTemplate)); } if (requiredLinkValues == null) { throw new ArgumentNullException(nameof(requiredLinkValues)); } var entry = new OutboundRouteEntry() { Handler = handler, Order = order, Precedence = RoutePrecedence.ComputeOutbound(routeTemplate), RequiredLinkValues = requiredLinkValues, RouteName = routeName, RouteTemplate = routeTemplate, }; var constraintBuilder = new RouteConstraintBuilder(_constraintResolver, routeTemplate.TemplateText); foreach (var parameter in routeTemplate.Parameters) { if (parameter.InlineConstraints != null) { if (parameter.IsOptional) { constraintBuilder.SetOptional(parameter.Name); } foreach (var constraint in parameter.InlineConstraints) { constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint); } } } entry.Constraints = constraintBuilder.Build(); entry.Defaults = new RouteValueDictionary(); foreach (var parameter in entry.RouteTemplate.Parameters) { if (parameter.DefaultValue != null) { entry.Defaults.Add(parameter.Name, parameter.DefaultValue); } } OutboundEntries.Add(entry); return entry; } /// <summary> /// Gets the list of <see cref="InboundRouteEntry"/>. /// </summary> public IList<InboundRouteEntry> InboundEntries { get; } = new List<InboundRouteEntry>(); /// <summary> /// Gets the list of <see cref="OutboundRouteEntry"/>. /// </summary> public IList<OutboundRouteEntry> OutboundEntries { get; } = new List<OutboundRouteEntry>(); /// <summary> /// Builds a <see cref="TreeRouter"/> with the <see cref="InboundEntries"/> /// and <see cref="OutboundEntries"/> defined in this <see cref="TreeRouteBuilder"/>. /// </summary> /// <returns>The <see cref="TreeRouter"/>.</returns> public TreeRouter Build() { return Build(version: 0); } /// <summary> /// Builds a <see cref="TreeRouter"/> with the <see cref="InboundEntries"/> /// and <see cref="OutboundEntries"/> defined in this <see cref="TreeRouteBuilder"/>. /// </summary> /// <param name="version">The version of the <see cref="TreeRouter"/>.</param> /// <returns>The <see cref="TreeRouter"/>.</returns> public TreeRouter Build(int version) { // Tree route builder builds a tree for each of the different route orders defined by // the user. When a route needs to be matched, the matching algorithm in tree router // just iterates over the trees in ascending order when it tries to match the route. var trees = new Dictionary<int, UrlMatchingTree>(); foreach (var entry in InboundEntries) { if (!trees.TryGetValue(entry.Order, out var tree)) { tree = new UrlMatchingTree(entry.Order); trees.Add(entry.Order, tree); } tree.AddEntry(entry); } return new TreeRouter( trees.Values.OrderBy(tree => tree.Order).ToArray(), OutboundEntries, _urlEncoder, _objectPool, _logger, _constraintLogger, version); } /// <summary> /// Removes all <see cref="InboundEntries"/> and <see cref="OutboundEntries"/> from this /// <see cref="TreeRouteBuilder"/>. /// </summary> public void Clear() { InboundEntries.Clear(); OutboundEntries.Clear(); } }
再创建TreeRouter
/// <summary> /// An <see cref="IRouter"/> implementation for attribute routing. /// </summary> public class TreeRouter : IRouter { // Key used by routing and action selection to match an attribute route entry to a // group of action descriptors. public static readonly string RouteGroupKey = "!__route_group"; private readonly LinkGenerationDecisionTree _linkGenerationTree; private readonly UrlMatchingTree[] _trees; private readonly IDictionary<string, OutboundMatch> _namedEntries; private readonly ILogger _logger; private readonly ILogger _constraintLogger; /// <summary> /// Creates a new instance of <see cref="TreeRouter"/>. /// </summary> /// <param name="trees">The list of <see cref="UrlMatchingTree"/> that contains the route entries.</param> /// <param name="linkGenerationEntries">The set of <see cref="OutboundRouteEntry"/>.</param> /// <param name="urlEncoder">The <see cref="UrlEncoder"/>.</param> /// <param name="objectPool">The <see cref="ObjectPool{T}"/>.</param> /// <param name="routeLogger">The <see cref="ILogger"/> instance.</param> /// <param name="constraintLogger">The <see cref="ILogger"/> instance used /// in <see cref="RouteConstraintMatcher"/>.</param> /// <param name="version">The version of this route.</param> internal TreeRouter( UrlMatchingTree[] trees, IEnumerable<OutboundRouteEntry> linkGenerationEntries, UrlEncoder urlEncoder, ObjectPool<UriBuildingContext> objectPool, ILogger routeLogger, ILogger constraintLogger, int version) { if (trees == null) { throw new ArgumentNullException(nameof(trees)); } if (linkGenerationEntries == null) { throw new ArgumentNullException(nameof(linkGenerationEntries)); } if (urlEncoder == null) { throw new ArgumentNullException(nameof(urlEncoder)); } if (objectPool == null) { throw new ArgumentNullException(nameof(objectPool)); } if (routeLogger == null) { throw new ArgumentNullException(nameof(routeLogger)); } if (constraintLogger == null) { throw new ArgumentNullException(nameof(constraintLogger)); } _trees = trees; _logger = routeLogger; _constraintLogger = constraintLogger; _namedEntries = new Dictionary<string, OutboundMatch>(StringComparer.OrdinalIgnoreCase); var outboundMatches = new List<OutboundMatch>(); foreach (var entry in linkGenerationEntries) { var binder = new TemplateBinder(urlEncoder, objectPool, entry.RouteTemplate, entry.Defaults); var outboundMatch = new OutboundMatch() { Entry = entry, TemplateBinder = binder }; outboundMatches.Add(outboundMatch); // Skip unnamed entries if (entry.RouteName == null) { continue; } // We only need to keep one OutboundMatch per route template // so in case two entries have the same name and the same template we only keep // the first entry. if (_namedEntries.TryGetValue(entry.RouteName, out var namedMatch) && !string.Equals( namedMatch.Entry.RouteTemplate.TemplateText, entry.RouteTemplate.TemplateText, StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException( Resources.FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(entry.RouteName), nameof(linkGenerationEntries)); } else if (namedMatch == null) { _namedEntries.Add(entry.RouteName, outboundMatch); } } // The decision tree will take care of ordering for these entries. _linkGenerationTree = new LinkGenerationDecisionTree(outboundMatches.ToArray()); Version = version; } /// <summary> /// Gets the version of this route. /// </summary> public int Version { get; } internal IEnumerable<UrlMatchingTree> MatchingTrees => _trees; /// <inheritdoc /> public VirtualPathData GetVirtualPath(VirtualPathContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } // If it's a named route we will try to generate a link directly and // if we can't, we will not try to generate it using an unnamed route. if (context.RouteName != null) { return GetVirtualPathForNamedRoute(context); } // The decision tree will give us back all entries that match the provided route data in the correct // order. We just need to iterate them and use the first one that can generate a link. var matches = _linkGenerationTree.GetMatches(context.Values, context.AmbientValues); if (matches == null) { return null; } for (var i = 0; i < matches.Count; i++) { var path = GenerateVirtualPath(context, matches[i].Match.Entry, matches[i].Match.TemplateBinder); if (path != null) { return path; } } return null; } /// <inheritdoc /> public async Task RouteAsync(RouteContext context) { foreach (var tree in _trees) { var tokenizer = new PathTokenizer(context.HttpContext.Request.Path); var root = tree.Root; var treeEnumerator = new TreeEnumerator(root, tokenizer); // Create a snapshot before processing the route. We'll restore this snapshot before running each // to restore the state. This is likely an "empty" snapshot, which doesn't allocate. var snapshot = context.RouteData.PushState(router: null, values: null, dataTokens: null); while (treeEnumerator.MoveNext()) { var node = treeEnumerator.Current; foreach (var item in node.Matches) { var entry = item.Entry; var matcher = item.TemplateMatcher; try { if (!matcher.TryMatch(context.HttpContext.Request.Path, context.RouteData.Values)) { continue; } if (!RouteConstraintMatcher.Match( entry.Constraints, context.RouteData.Values, context.HttpContext, this, RouteDirection.IncomingRequest, _constraintLogger)) { continue; } _logger.RequestMatchedRoute(entry.RouteName, entry.RouteTemplate.TemplateText); context.RouteData.Routers.Add(entry.Handler); await entry.Handler.RouteAsync(context); if (context.Handler != null) { return; } } finally { if (context.Handler == null) { // Restore the original values to prevent polluting the route data. snapshot.Restore(); } } } } } } private VirtualPathData GetVirtualPathForNamedRoute(VirtualPathContext context) { if (_namedEntries.TryGetValue(context.RouteName, out var match)) { var path = GenerateVirtualPath(context, match.Entry, match.TemplateBinder); if (path != null) { return path; } } return null; } private VirtualPathData GenerateVirtualPath( VirtualPathContext context, OutboundRouteEntry entry, TemplateBinder binder) { // In attribute the context includes the values that are used to select this entry - typically // these will be the standard 'action', 'controller' and maybe 'area' tokens. However, we don't // want to pass these to the link generation code, or else they will end up as query parameters. // // So, we need to exclude from here any values that are 'required link values', but aren't // parameters in the template. // // Ex: // template: api/Products/{action} // required values: { id = "5", action = "Buy", Controller = "CoolProducts" } // // result: { id = "5", action = "Buy" } var inputValues = new RouteValueDictionary(); foreach (var kvp in context.Values) { if (entry.RequiredLinkValues.ContainsKey(kvp.Key)) { var parameter = entry.RouteTemplate.GetParameter(kvp.Key); if (parameter == null) { continue; } } inputValues.Add(kvp.Key, kvp.Value); } var bindingResult = binder.GetValues(context.AmbientValues, inputValues); if (bindingResult == null) { // A required parameter in the template didn't get a value. return null; } var matched = RouteConstraintMatcher.Match( entry.Constraints, bindingResult.CombinedValues, context.HttpContext, this, RouteDirection.UrlGeneration, _constraintLogger); if (!matched) { // A constraint rejected this link. return null; } var pathData = entry.Handler.GetVirtualPath(context); if (pathData != null) { // If path is non-null then the target router short-circuited, we don't expect this // in typical MVC scenarios. return pathData; } var path = binder.BindValues(bindingResult.AcceptedValues); if (path == null) { return null; } return new VirtualPathData(this, path); } }