Preparing ASP.NET5, Issue No. 4 - Details on Routing

    We continue our ASP.NET5 column with a publication from Stanislav Boyarintsev ( masterL ), a developer of corporate web systems from ItWebNet. In this article, Stanislav talks in great detail and interestingly about the routing mechanism in ASP.NET5. Previous articles from the column can always be read at #aspnetcolumn - Vladimir Yunev


    How the routing system to ASP.NET 5 was organized


    Routing to ASP.NET 5 was done using the ASP.NET module UrlRoutingModule . The module went through a collection of routes (usually objects of the Route class ) stored in the static Routes property of the RouteTable class , selected a route that matched the current request and called the route handler, which was stored in the RouteHandler property of the Route class - each registered route could have its own handler. In the MVC application, this handler was MvcRouteHandler , which took over the further work with the request.

    We added the routes to the RouteTable.Routes collection in the application configuration process.

    Typical routing system configuration code in an MVC application:

    RouteTable.Routes.MapRoute(
                    name: "Default",
                    url: "{controller}/{action}/{id}",
                    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
    

    Where MapRoute is an extension method declared in the System.Web.Mvc namespace that added a new route to the route collection in the Routes property using MvcRouteHandler as a handler.

    We could do this on our own:

    RouteTable.Routes.Add(new Route(
        url: "{controller}/{action}/{id}",
        defaults: new RouteValueDictionary(new { controller = "Home", action = "Index", id = UrlParameter.Optional }),
        routeHandler: new MvcRouteHandler())
    );
    

    How the routing system is organized in ASP.NET 5: Short version


    ASP.NET 5 no longer uses modules; for processing requests, “middleware” introduced as part of the transition to OWIN - “Open Web Interface” - which allows running ASP.NET 5 applications not only on the IIS server, is used.

    So now routing is done using RouterMiddleware . The entire routing project can be downloaded from github . Within the framework of this concept, the request is transferred from one middleware to another, in the order of their registration at application startup. When the request reaches RouterMiddleware it compares whether the requested Url address is suitable for any registered route, and if it does, it calls the handler of this route.

    How the routing system is organized in ASP.NET 5: Long version


    In order to understand how the routing system works, let's connect it to an empty ASP.NET 5 project.

    1. Create an empty ASP.NET 5 project (by choosing Empty Template) and name it “AspNet5Routing”.
    2. Add in the “dependencies” of the project in the project.json file “Microsoft.AspNet.Routing”:

      "dependencies": {
          "Microsoft.AspNet.Server.IIS": "1.0.0-beta5",
          "Microsoft.AspNet.Server.WebListener": "1.0.0-beta5",
          "Microsoft.AspNet.Routing": "1.0.0-beta5"
        },
      

    3. In the Startup.cs file, add the use of the Microsoft.AspNet.Routing namespace:

      using Microsoft.AspNet.Routing;
      

    4. Add the necessary services (services that the routing system uses in its work) in the ConfigureServices () method of the Startup.cs file:

      public void ConfigureServices(IServiceCollection services)
      {
          services.AddRouting();
      }
      

    5. Finally, we configure the routing system in the Configure () method of the Startup.cs file:

      public void Configure(IApplicationBuilder app)
      {
          var routeBuilder = new RouteBuilder();
          routeBuilder.DefaultHandler = new ASPNET5RoutingHandler();
          routeBuilder.ServiceProvider = app.ApplicationServices;
          routeBuilder.MapRoute("default", "{controller}/{action}/{id}");
          app.UseRouter(routeBuilder.Build());
      }
      

    Taken from the example in the routing project .

    We will analyze the last step in more detail:

    var routeBuilder = new RouteBuilder();
    routeBuilder.DefaultHandler = new ASPNET5RoutingHandler();
    routeBuilder.ServiceProvider = app.ApplicationServices;
    

    Create an instance of RouteBuilder and fill in its properties. Of interest is the DefaultHandler property of type IRouter - judging by the name, it should contain a request handler. I put in it an instance of ASPNET5RoutingHandler - the request handler I invented, let's create it:

    using Microsoft.AspNet.Routing;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNet.Http;
    namespace AspNet5Routing
    {
        public class ASPNET5RoutingHandler : IRouter
        {
            public VirtualPathData GetVirtualPath(VirtualPathContext context)
            {
            }
            public async Task RouteAsync(RouteContext context)
            {
                await context.HttpContext.Response.WriteAsync("ASPNET5RoutingHandler work");
                context.IsHandled = true;
            }
        }
    }
    

    The IRouter interface requires us only two methods GetVirtualPath and RouteAsync.

    GetVirtualPath method - we are familiar from previous versions of ASP.NET; it was in the interface of the RouteBase class from which the Route class representing a route is inherited . This method was responsible for building Url (for example, when we called the ActionLink method: Html .ActionLink ("link", "Index")).

    And in the RouteAsync method - we process the request and write the result of the processing in Response.

    The following line of the Configure method:

    routeBuilder.MapRoute("default", "{controller}/{action}/{id}");
    

    Like two drops of water, it is similar to using the MapRoute method in MVC 5, its parameters are the name of the added route and the template with which the requested Url will be mapped.

    MapRoute () itself, like in MVC 5, is an extension method , and its call ultimately boils down to Creating an instance of the TemplateRoute class and adding it to the Routes collection of our RouteBuilder object:

    routeBuilder.Routes.Add(new TemplateRoute(routeCollectionBuilder.DefaultHandler,
                                                    name, // в нашем случае передается "default"
                                                    template, // в нашем случае передается "{controller}/{action}/{id}"
                                                    ObjectToDictionary(defaults),
                                                    ObjectToDictionary(constraints),
                                                    ObjectToDictionary(dataTokens),
                                                    inlineConstraintResolver));
    

    Interestingly, the Routes property is the IRouter collection, that is, TemplateRoute also implements the IRouter interface, like the ASPNET5RoutingHandler we created, by the way, it is passed to the TemplateRoute constructor.

    And finally, the last line:

    app.UseRouter(routeBuilder.Build());
    

    Call routeBuilder.Build () - creates an instance of the RouteCollection class and adds to it all the elements from the Route property of the RouteBuilder class.

    And app.UseRouter () is an extension method that actually connects RouterMiddleware to the request processing pipeline, passing it the RouteCollection object created and filled in the Build () method.

    public static IApplicationBuilder UseRouter([NotNull] this IApplicationBuilder builder, [NotNull] IRouter router)
    {
        return builder.UseMiddleware(router);
    }
    

    And judging by the constructor of RouterMiddleware:

    public RouterMiddleware(
        RequestDelegate next,
        ILoggerFactory loggerFactory,
        IRouter router)
    

    The RouteCollection object also implements the IRouter interface, as does ASPNET5RoutingHandler with TemplateRoute.

    So we got the following nesting doll:

    Our ASPNET5RoutingHandler request handler is packaged in TemplateRoute , TemplateRoute itself or several instances of TemplateRoute (if we called the MapRoute () method several times) are packaged in RouteCollection , and the RouteCollection is passed to the RouterMiddleware constructor and stored in it.

    This completes the process of setting up the routing system, you can start the project, go to the address: "/ Home / Index / 1" and see the result: "ASPNET5RoutingHandler work".

    Well, let's briefly go over what happens with the routing system during the incoming request:

    When the queue reaches RouterMiddleware , in the list of middleware launched, it calls the RouteAsync () method on the saved IRouter instance - this is an object of the RouteCollection class .

    The RouteCollection, in turn, passes through the IRouter instances stored in it - in our case it will be TemplateRoute and calls the RouteAsync () method on them .

    TemplateRoutechecks if the requested Url matches its template (passed in the TemplateRoute constructor: "{controller} / {action} / {id}") and if it matches, calls the IRouter instance stored in it - which is our ASPNET5RoutingHandler .

    We connect the routing system to the MVC application


    Now let's see how the MVC Framework communicates with the routing system.

    Again, create an empty ASP.NET 5 project using the Empty template.

    1. Add in the “dependencies” of the project in the project.json file “Microsoft.AspNet.Mvc”:

      "dependencies": {
          "Microsoft.AspNet.Server.IIS": "1.0.0-beta5",
          "Microsoft.AspNet.Server.WebListener": "1.0.0-beta5",
          "Microsoft.AspNet.Mvc": "6.0.0-beta5"
        },
      

    2. In the Startup.cs file , add the use of the Microsoft.AspNet.Builder namespace :

      using Microsoft.AspNet.Builder;
      

    The extensions methods we need to connect MVC are in it.

    1. Add the services that MVC Framework uses in its work: in the ConfigureServices () method of the Startup.cs file:

      public void ConfigureServices(IServiceCollection services)
      {
          services.AddMvc();
      }
      

    2. We configure the MVC application in the Configure () method of the Startup.cs file:

    Three different methods are available to us:

    1.

        public void Configure(IApplicationBuilder app)
        {
            app.UseMvc()
        }
    

    2.

        public void Configure(IApplicationBuilder app)
        {
            app.UseMvcWithDefaultRoute()
        }
    

    3.

        public void Configure(IApplicationBuilder app)
        {
            return app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    

    Let's see the implementation of these methods right away :

    The first method:

        public static IApplicationBuilder UseMvc(this IApplicationBuilder app)
        {
            return app.UseMvc(routes =>
            {
            });
        }
    

    Invokes the third method, passing an Action delegatethat does nothing.

    Second method:

        public static IApplicationBuilder UseMvcWithDefaultRoute(this IApplicationBuilder app)
        {
            return app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    

    Also calls the third method, only in the Action delegatedefault route is added.

    Third method:

        public static IApplicationBuilder UseMvc(
            this IApplicationBuilder app,
            Action configureRoutes)
        {
            MvcServicesHelper.ThrowIfMvcNotRegistered(app.ApplicationServices);
            var routes = new RouteBuilder
            {
                DefaultHandler = new MvcRouteHandler(),
                ServiceProvider = app.ApplicationServices
            };
            configureRoutes(routes);
            // Adding the attribute route comes after running the user-code because
            // we want to respect any changes to the DefaultHandler.
            routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(
                routes.DefaultHandler,
                app.ApplicationServices));
            return app.UseRouter(routes.Build());
        }
    

    It does the same thing as in the previous section when registering a route for our handler, it only sets up an instance of MvcRouteHandler as the final handler and makes a call to the CreateAttributeMegaRoute method - which is responsible for adding the routes set using the attributes of the controllers and action methods (Attribute-Based routing )

    Thus, all three methods will include routing in our Attribute-Based application, but in addition, calling the second method will add a default route, and the third method allows you to specify any routes we need by transferring them using a delegate (Convention-Based routing).

    Convention-Based Routing


    As I wrote above - it is configured by calling the MapRoute () method - and the process of using this method has not changed since MVC 5 - we can pass the route name, its template, default values ​​and restrictions to the MapRoute () method .

    routeBuilder.MapRoute("regexStringRoute", //name
                          "api/rconstraint/{controller}", //template
                          new { foo = "Bar" }, //defaults
                          new { controller = new RegexRouteConstraint("^(my.*)$") }); //constraints
    

    Attribute-Based Routing


    Unlike MVC 5, where attribute-based routing had to be specifically enabled, it was enabled by default in MVC 6.

    It should also be remembered that routes defined using attributes take precedence in finding matches and choosing the appropriate route (compared to convention-based routes).

    To specify a route, you must use the Route attribute for both the action methods and the controller (in MVC 5, the RoutePrefix attribute was used for the controller to specify the route ).

    [Route("appointments")]
    public class Appointments : ApplicationBaseController
    {
        [Route("check")]
        public IActionResult Index()
        {
            return new ContentResult
            {
                Content = "2 appointments available."
            };
        }
    }
    

    As a result, this action method will be available at: "/ appointments / check".

    Routing system setup


    ASP.NET 5 introduced a new service configuration mechanism called Options - GitHub project . It allows you to make some settings for the routing system.

    The meaning of its work is that when you configure the application in the Startup.cs file , we transfer an object with the properties defined in a certain way to the dependency registration system, and while the application is running, this object gets and depending on the values ​​of the exposed properties, the application builds its work.

    The RouteOptions class is used to configure the routing system .

    For convenience, we have the ConfigureRouting extension method available :

    public void ConfigureServices(IServiceCollection services)
    {
        services.ConfigureRouting(
            routeOptions =>
            {
                routeOptions.LowercaseUrls = true; // генерация url в нижнем регистре
                routeOptions.AppendTrailingSlash = true; // добавление слеша в конец url
            });
    }
    

    Behind the scenes, he simply makes a call to the Configure method passing the Action delegate to it:

    public static void ConfigureRouting(
        this IServiceCollection services,
        Action setupAction)
    {
        if (setupAction == null)
        {
            throw new ArgumentNullException(nameof(setupAction));
        }
        services.Configure(setupAction);
    }
    

    Route pattern


    The principles of working with the route pattern remained the same as they were in MVC 5:

    • Address segments are separated by a slash: firstSegment / secondSegment .
    • The constant part of the segment is considered if it is not bordered with curly brackets, such a route matches the requested Url, only if the address contains exactly the same values: firstSegment / secondSegment - such a route corresponds only to an address of the form: siteDomain / firstSegment / secondSegment .
    • The variable parts of the segment are taken in braces: firstSegment / {secondSegment} - the pattern will correspond to any two segment addresses, where the first segment is “firstSegment”, and the second segment can be any character set (except for a slash - as this will indicate the beginning of the third segment ):

      "/ firstSegment / index"

      "/ firstSegment / index-2"

    • The restrictions for the variable part of the segment, as the name implies, limit the permissible values ​​of the variable segment and are set after the ":" symbol. Several restrictions can be imposed on one variable part; parameters are passed using parentheses: firstSegment / {secondSegment: minlength (1): maxlength (3)} . The string designation for restrictions can be found in the GetDefaultConstraintMap () method of the RouteOptions class .
    • In order to make the last segment “greedy”, so that it will absorb the entire remaining line of the address, you need to use the * character: {controller} / {action} / {* allRest} - will correspond to the address: "/ home / index / 2 ", and the address:" / home / index / 2/4/5 ".

    But in ASP.NET 5, the route pattern got some additional features:

    1. The ability to set directly in it the default values ​​for the variable parts of the route: {controller = Home} / {action = Index} .
    2. Set the optionalness of the variable part of the segment using the?: {Controller = Home} / {action = Index} / {id?} Symbol .

    Also, when using the route template in the attributes, changes occurred:

    When configuring routing through the attributes, the parameters denoting the controller and the action method should now be addressed by square brackets and using the words “controller” and “action”: "[controller] / [ action] "- and you can use them only in this form - neither the default values, nor restrictions, nor optionality, nor greed are allowed.

    That is, it is allowed:

    Route("[controller]/[action]/{id?}")
    Route("[controller]/[action]")
    

    You can use them individually:

    Route("[controller]")
    Route("[action]")
    

    Not allowed:

    Route("{controller}/{action}")
    Route("[controller=Home]/[action]")
    Route("[controller?]/[action]")
    Route("[controller]/[*action]")
    

    The general scheme of the route pattern looks like this:

    constantPart-{variablePart}/{paramName:constraint1:constraint2=DefaultValue?}/{*lastGreedySegment}
    

    Conclusion


    In this article, we went over the ASP.NET 5 routing system, looked at how it was organized, and connected it to an empty ASP.NET 5 project using our route handler. We examined how to connect it to the MVC application and configure it using the Options engine. We dwell on the changes that have occurred in the use of Attribute-Based routing and the route pattern.

    For authors


    Friends, if you are interested in supporting the column with your own material, please write to me at vyunev@microsoft.com in order to discuss all the details. We are looking for authors who can interestingly talk about ASP.NET and other topics.

    about the author


    Boyarintsev Stanislav Aleksandrovich
    Leading .NET programmer in ItWebNet, the city of Kirov
    masterL

    .NET programmer with 4 years of experience. He is engaged in the development of corporate web systems. Professional Interests: ASP.NET MVC.
    Blog: boyarincev.net

    Only registered users can participate in the survey. Please come in.

    Your attitude to a new approach to routing in ASP.NET5

    • 66.6% Positive, OWIN is good 98
    • 30.6% Restrained until decided 45
    • 2.7% Negative, used to be better 4

    Also popular now: