Understanding How Routing Works in NopCommerce Plugins

A Sample NopCommerce Route

Note: Basic working knowledge of MVC routing is assumed for this article – this is not a tutorial on what routing is or how it works, but rather a light analysis of how NopCommerce plugin routing is architected

Routing can sometimes be a challenging or confusing topic in .NET development.  There are a lot of pieces that fit together to make things work right, especially with MVC.  When working with a huge platform like NopCommerce, this is especially true due to the need for extensibility.  NopCommerce allows developers to write their own plugins and integrate them into the NopCommerce routing system.  Adding routes into your Plugin can be confusing, so let’s take a look at how this all works.  Below I have pasted a sample route from the NopCommerce plugin tutorial.

routes.MapRoute("Plugin.Payments.PayPalStandard.PDTHandler",
     "Plugins/PaymentPayPalStandard/PDTHandler",
     new { controller = "PaymentPayPalStandard", action = "PDTHandler" },
     new[] { "Nop.Plugin.Payments.PayPalStandard.Controllers" }
);

This is a pretty standard MVC (and NopCommerce) route.  MapRoute is a shorthand method that returns a new Route object that implements the default MVC Route Handler.  If we quickly analyze the parameters in order we can see there is nothing special going on here:

  • The name of the route, in this case described by the namespace and class.
  • The url path that the plugin maps to
  • The default values for the controller and action.  In this scenario the controller and action are not variable sections in the URL – they will always be PaymentPayPalStandard and PDTHandler for that URL.
  • Finally we pass in an anonymous object that specifices a namespace.  This can be important in MVC projects and is certainly important in NopCommerce.  This parameter instructs MVC to prioritize controllers found in the namespace we passed in for the given route.  For example, if your MVC project has two home controllers, you can identify which one a certain route should use by specifying a namespace. This becomes increasingly important in extensible scenarious like NopCommerce, where multiple plugins might have a similar controller name such as “AdminController” or something.  As a side note, if you pass in multiple Namespaces they are treated with equal priority, meaning if you have two namespaces you want to pass in as first and second priority you need to define two routes.

So basically, remember to include a namespace parameter in your routes for MVC plugins – this helps Nop determine that your controller is the right one to use for your route and avoids collisions with other plugins.

Understanding how plugin routes are discovered and registered

Let’s now take a look at how NopCommerce actually discovers, registers and uses all of the various routes that plugins can define.

Inside most MVC projects, routes are registered in either the App_Start/RouteConfig.cs file (in MVC 4 and 5) or simply in the Application_Start method of the global.asax file.  Application_Start is one of a few different application life cycle events that are fired every time a new application instance is started.  This is not something specific to MVC – this is just part of the ASP.NET platform.  Remember, routing in general is part of ASP.NET and is not MVC specific.  NopCommerce is really no different in this regard.

Nop.Web is the project that actually runs while you are using NopCommerce, so if we look inside this project we should see a global.asax file.  Let’s open it up and browser to the Application_Start() method.  In the middle of this method we should see a line of code that registers our routes.

RegisterRoutes(RouteTable.Routes);

Notice that RouteTable.Routes is passed into this method as usual.  RouteTable is a static class that our application can use on start up to add all of the routes we want to define that will then be used while the application is running. The RegisterRoutes method is defined in the same global.asax file, seen below.

public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("favicon.ico");
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
            
            //register custom routes (plugins, etc)
            var routePublisher = EngineContext.Current.Resolve<IRoutePublisher>();
            routePublisher.RegisterRoutes(routes);
            
            routes.MapRoute(
                "Default"// Route name
                "{controller}/{action}/{id}"// URL with parameters
                new { controller = "Home", action = "Index", id = UrlParameter.Optional },
                new[] { "Nop.Web.Controllers" }
            );
        }

The RegisterRoutes method is where the NopCommerce magic begins to happen.  Let’s look at the middle two lines in particular beneath the comment “//register custom routes (plugins, etc)”.  First NopCommerce uses its dependency injection engine to request an instance of an object that implements IRoutePublisher, which in this case is RoutePublisher.cs.  We then call the routePublisher’s own RegisterRoutes method .  Remember, this is a different RegisterRoutes method than the one we are calling in global.asax.  We again pass in the RouteTable.Routes collection we passed into the first RegisterRoutes method to make the RouteTable available for adding routes.  Let’s see what happens inside the RoutePublisher.cs RegisterRoutes method.

public virtual void RegisterRoutes(RouteCollection routes)
       {
           var routeProviderTypes = typeFinder.FindClassesOfType<IRouteProvider>();
           var routeProviders = new List<IRouteProvider>();
           foreach (var providerType in routeProviderTypes)
           {
               //Ignore not installed plugins
               var plugin = FindPlugin(providerType);
               if (plugin != null && !plugin.Installed)
                   continue;
 
               var provider = Activator.CreateInstance(providerType) as IRouteProvider;
               routeProviders.Add(provider);
           }
           routeProviders = routeProviders.OrderByDescending(rp => rp.Priority).ToList();
           routeProviders.ForEach(rp => rp.RegisterRoutes(routes));
       }

This is really where all of the power of NopCommerce plugin routing resides.  This method calls another very important method – the only other method defined in RoutePublisher.cs.  We can see the FindPlugin method below:

protected virtual PluginDescriptor FindPlugin(Type providerType)
        {
            if (providerType == null)
                throw new ArgumentNullException("providerType");
 
            foreach (var plugin in PluginManager.ReferencedPlugins)
            {
                if (plugin.ReferencedAssembly == null)
                    continue;
 
                if (plugin.ReferencedAssembly.FullName == providerType.Assembly.FullName)
                    return plugin;
            }
 
 
 
            return null;
        }

Let’s review what’s going on here in these two classes.  Inside our RegisterRoutes method, NopCommerce uses the typeFinder and C# reflection to find all of the classes that implement the interface IRouteProvider.  All NopCommerce plugins should have a RouteConfig.cs file that implements IRouteProvider, which defines a RegisterRoutes method.  After it has collected all of the types that implement this interface, it then uses the FindPlugin method to check if the corresponding plugin for each RouteProvider is installed.  If the plugin really is installed, it proceeds to create an instance of each class that implements IRouteProvider and calls the RegisterRoutes method of each.  This effectively loops through all the plugins in your NopCommerce installation and adds all of the defined plugin routes to the RouteTable.

All of these routes are then registered with the application during the Application_Start lifecycle event and can be usd by the ASP.NET routing module to match incoming requests to the routes you have defined (the MVC Routing Module is beyond the scope of this article).

For example, the FacebookShop plugin defines a RouteProvider class that implements IRouteProvider and defines the following RegisterRoutes method:

public void RegisterRoutes(RouteCollection routes)
        {
            //home page
            routes.MapRoute("Plugin.Misc.FacebookShop.Index",
                 "facebook/shop/",
                 new { controller = "MiscFacebookShop", action = "Index" },
                 new[] { "Nop.Plugin.Misc.FacebookShop.Controllers" }
            );
 
            //category page
            routes.MapRoute("Plugin.Misc.FacebookShop.Category",
                 "facebook/shop/category/{categoryId}/",
                 new { controller = "MiscFacebookShop", action = "Category" },
                 new { categoryId = @"\d+" },
                 new[] { "Nop.Plugin.Misc.FacebookShop.Controllers" }
            );
 
            //search page
            routes.MapRoute("Plugin.Misc.FacebookShop.ProductSearch",
                 "facebook/shop/search/",
                 new { controller = "MiscFacebookShop", action = "Search" },
                 new[] { "Nop.Plugin.Misc.FacebookShop.Controllers" }
            );
        }

This plugin adds three routes, all of which will be added to the RouteTable when an instance of this RouteProvider class is created and the RegisterRoutes method is called.  The URLs defined in those routes will then be available for use in the application.

Hopefully this provides some insight into how routing works in NopCommerce plugins.

No Comments

Comments Closed