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);
        }
    }

 

posted @ 2020-04-06 15:09  蓝平凡  阅读(1161)  评论(0编辑  收藏  举报