ASP.NET Core has adopted a new routing scheme called Endpoint since version 2.2, which is not much different from the original scheme, but the internal operation is quite different. The previous article introduced the operation mechanism of the original routing scheme in detail. This article still uses a picture to understand the new version of the operating mechanism, and finally summarizes the similarities and differences between the two.

1 Overview

This scheme starts from version 2.2 and is called endpoint routing ( hereafter referred to as “new version” ). It is enabled by default. If you want to use the original scheme ( <=2.1, the original version is called below ), you can use AddMvc. Make settings

services.AddMvc(option=>option.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

 

EnableEndpointRouting defaults to true, which means that the new Endpoint scheme is enabled. If set to false, the legacy (<=2.1) routing scheme is used.

In terms of configuration method, the system is still configured in use.Mvc() in Startup. In fact, the internal processing middleware has been changed from the original RouterMiddleware to EndpointMiddleware and EndpointRoutingMiddleware. The following still passes a picture. Take a detailed look at the picture.

2 the process and analysis

1. Initial configuration of the route (the first two lanes of the figure) 

1 Everything is still starting from Startup, and like the old version, it is configured by UseMvc method, and one or more configurations such as routes.MapRoute(…) are not mentioned.Let’s focus on the following process, take a look at the UseMvc method in MvcApplicationBuilderExtensions:

public static IApplicationBuilder UseMvc(
    this IApplicationBuilder app,
    Action<IRouteBuilder> configureRoutes)
{
//
    var options = app.ApplicationServices.GetRequiredService<IOptions<MvcOptions>>();
    if (options.Value.EnableEndpointRouting)
    {
        var mvcEndpointDataSource = app.ApplicationServices
            .GetRequiredService<IEnumerable<EndpointDataSource>>()
            .OfType<MvcEndpointDataSource>()
            .First();
        var parameterPolicyFactory = app.ApplicationServices
            .GetRequiredService<ParameterPolicyFactory>();

        var endpointRouteBuilder = new EndpointRouteBuilder(app);

        configureRoutes(endpointRouteBuilder);

        foreach (var router in endpointRouteBuilder.Routes)
        {
            // Only accept Microsoft.AspNetCore.Routing.Route when converting to endpoint
            // Sub-types could have additional customization that we can't knowingly convert
            if (router is Route route && router.GetType() == typeof(Route))
            {
                var endpointInfo = new MvcEndpointInfo(
                    route.Name,
                    route.RouteTemplate,
                    route.Defaults,
                    route.Constraints.ToDictionary(kvp => kvp.Key, kvp => (object)kvp.Value),
                    route.DataTokens,
                    parameterPolicyFactory);
             mvcEndpointDataSource.ConventionalEndpointInfos.Add(endpointInfo);
            }
            else
            {
                throw new InvalidOperationException($"Cannot use '{router.GetType().FullName}' with Endpoint Routing.");
            }
        }
        if (!app.Properties.TryGetValue(EndpointRoutingRegisteredKey, out _))
        {
            // Matching middleware has not been registered yet
            // For back-compat register middleware so an endpoint is matched and then immediately used
            app.UseEndpointRouting();
        }
        return app.UseEndpoint();
    }
    else
    {
       //old
    }
}

2 Line 6, where the value of EnableEndpointRouting is set and judged. If it is false, the old version is used. See the previous article for details. The value is true by default, that is, the new version is used.
3 Corresponding to the 9th line, MvcEndpointDataSource is a very important role in the new version of the route. In the startup initialization phase, it completes the routing table storage and conversion. Here, first mark it with color, let’s remember it, in the following process. Detailed in the introduction.
4 corresponds to the 16th line, like the old version of RouteBuilder, here will be a new endpointRouteBuilder, both of which are an IRouteBuilder, so also call the configureRoutes (endpointRouteBuilder) method (that is, the configuration in the startup) to get a collection of Route (IList <IRouter>) is assigned to endpointRouteBuilder.Routes. Here is a special place to note if (router is Route route && router.GetType() == typeof(Route)), that is, only route type is accepted here, and the endpoint routing system IRouter-based extensibility is not supported, including inheritance from Route.
5 Corresponding to the 20th line, the endpointRouteBuilder.Routes just obtained is traversed, converted into a set of MvcEndpointInfo, and assigned to mvcEndpointDataSource.ConventionalEndpointInfos.
6 After that, the middleware is plugged into the pipeline. The processing middleware here is changed from the original RouterMiddleware to EndpointMiddleware and EndpointRoutingMiddleware.

       2. Processing of the request (the last two lanes of the figure)

Most of the functions of the request are in the middleware EndpointRoutingMiddleware. He has an important attribute _endpointDataSource to save the MvcEndpointDataSource generated in the initialization phase above, and the middleware EndpointMiddleware function is relatively simple, mainly after EndpointRoutingMiddleware filters out the endpoint and calls the endpoint. The endpoint.RequestDelegate(httpContext) handles the request.
7 The InitializeAsync() method is mainly used to call InitializeCoreAsync() to create a matcher. The code of this method shows that it is only executed once when the first request is made.

private Task<Matcher> InitializeAsync()
{
var initializationTask = _initializationTask;
if (initializationTask != null)
{
return initializationTask;
}

return InitializeCoreAsync();
}

8 MvcEndpointDataSource An important method, UpdateEndpoints(), is to read all actions and match this action list with its ConventionalEndpointInfos list (see 5) to generate a new list. As shown below, we only configure a route such as “{controller=Home}/{action=Index}/{id?}” by default. The default HomeController has three actions, adds a controller named FlyLoloController and adds it. An action with attribute routing finally generates 7 Endpoints, which is a bit like the “product” of routing and action. Of course, here is just a simple example with the default program. There may be more routing templates registered in the actual project, there will be more Controller and Action, and attribute routing.

The specific code is as follows:

private void UpdateEndpoints()
        {
            lock (_lock)
            {
                var endpoints = new List<Endpoint>();
                StringBuilder patternStringBuilder = null;

                foreach (var action in _actions.ActionDescriptors.Items)
                {
                    if (action.AttributeRouteInfo == null)
                    {
                        // In traditional conventional routing setup, the routes defined by a user have a static order
                        // defined by how they are added into the list. We would like to maintain the same order when building
                        // up the endpoints too.
                        //
                        // Start with an order of '1' for conventional routes as attribute routes have a default order of '0'.
                        // This is for scenarios dealing with migrating existing Router based code to Endpoint Routing world.
                        var conventionalRouteOrder = 1;

                        // Check each of the conventional patterns to see if the action would be reachable
                        // If the action and pattern are compatible then create an endpoint with the
                        // area/controller/action parameter parts replaced with literals
                        //
                        // e.g. {controller}/{action} with HomeController.Index and HomeController.Login
                        // would result in endpoints:
                        // - Home/Index
                        // - Home/Login
                        foreach (var endpointInfo in ConventionalEndpointInfos)
                        {
                            // An 'endpointInfo' is applicable if:
                            // 1. it has a parameter (or default value) for 'required' non-null route value
                            // 2. it does not have a parameter (or default value) for 'required' null route value
                            var isApplicable = true;
                            foreach (var routeKey in action.RouteValues.Keys)
                            {
                                if (!MatchRouteValue(action, endpointInfo, routeKey))
                                {
                                    isApplicable = false;
                                    break;
                                }
                            }

                            if (!isApplicable)
                            {
                                continue;
                            }

                            conventionalRouteOrder = CreateEndpoints(
                                endpoints,
                                ref patternStringBuilder,
                                action,
                                conventionalRouteOrder,
                                endpointInfo.ParsedPattern,
                                endpointInfo.MergedDefaults,
                                endpointInfo.Defaults,
                                endpointInfo.Name,
                                endpointInfo.DataTokens,
                                endpointInfo.ParameterPolicies,
                                suppressLinkGeneration: false,
                                suppressPathMatching: false);
                        }
                    }
                    else
                    {
                        var attributeRoutePattern = RoutePatternFactory.Parse(action.AttributeRouteInfo.Template);

                        CreateEndpoints(
                            endpoints,
                            ref patternStringBuilder,
                            action,
                            action.AttributeRouteInfo.Order,
                            attributeRoutePattern,
                            attributeRoutePattern.Defaults,
                            nonInlineDefaults: null,
                            action.AttributeRouteInfo.Name,
                            dataTokens: null,
                            allParameterPolicies: null,
                            action.AttributeRouteInfo.SuppressLinkGeneration,
                            action.AttributeRouteInfo.SuppressPathMatching);
                    }
                }

                // See comments in DefaultActionDescriptorCollectionProvider. These steps are done
                // in a specific order to ensure callers always see a consistent state.

                // Step 1 - capture old token
                var oldCancellationTokenSource = _cancellationTokenSource;

                // Step 2 - update endpoints
                _endpoints = endpoints;

                // Step 3 - create new change token
                _cancellationTokenSource = new CancellationTokenSource();
                _changeToken = new CancellationChangeToken(_cancellationTokenSource.Token);

                // Step 4 - trigger old token
                oldCancellationTokenSource?.Cancel();
            }
        }

The essence is to calculate a request endpoint that may be requested, that is, an Endpoint. Thus, as in the previous article, I want to customize a handler to handle special templates (such as routes.MapRoute(“flylolo/{code}/{name}”, MyRouteHandler.Handler);) will be ignored, because It can’t generate Endpoint, and this way can be customized by customizing a middleware, no need to mix in the route.

9 is to use the Matcher generated above, carry the Endpoint list to match the request URL, and assign the matching Endpoint to the feature.Endpoint.
10 Get the feature.Endpoint, if it exists, call its RequestDelegate to process the request httpContext.

3 the new version and the old version of the similarities and differences summary

Briefly talk about the difference between the two versions from the two stages of application startup and request processing:

1. Start-up phase:

Most of the stages are similar. They are configured with a routing table by Startup’s app.UseMvc() method, a Route collection of Routes(IList<IRouter>), and then simply convert it.

<=2.1: Converting Routes to RouteCollection

2.2+ : Convert Routes to List<MvcEndpointInfo>

The difference between the two is not big. Although the names are different, they are basically the same. They can still be understood as the packaging of the collection of Route.

2. Request processing stage:

<=2.1: 1. Match the requested URL with the route template recorded in the RouteCollection.

2. After finding the matching Route, determine whether there is a corresponding Controlled and Action according to the URL of the request.

3. If the above passes, call the Route Handler to process the HttpContext.

2.2+ : 1. When processing the request for the first time, first make a match according to the route set List<MvcEndpointInfo> and _actions.ActionDescriptors.Items (the information of all actions) configured in the startup phase, and generate a list. This list is stored. All URL templates that may be matched, as shown in Figure 2, this list is also List<MvcEndpointInfo>, which records all possible URL patterns. It actually lists the detailed addresses that can be accessed, which is the final address. That is, the endpoint, perhaps why it is called Endpoint routing.

2. The requested Url matches the generated table to find the corresponding MvcEndpointInfo.

3. Call the RequestDelegate method of the matched MvcEndpointInfo to process the request.

The difference between the two is that for the matching of _actions.ActionDescriptors.Items (the information of all actions), the original version is first matched according to the routing template, and then according to ActionDescriptors to determine whether there is a corresponding Controller and action, and the new version first uses the action The information matches the routing template and then matches with the requested URL. Since this kind of work is only performed at the first request, although it is not tested for efficiency, it should feel faster than before.