Override GenericPathRoute in order to modify URL

9 months ago
Hello!
I am developing a plugin in NopCommerce 4.20 version and all I want to do is to change the URL template in Categories.
For example, if I am in homepage and click on specific category ,the URL should be www.mysite.com/categoryname/param/param2  rather than www.mysite.com/categoryname, where param and param2 are information to user.
The problem is that the RouteAsync method in CustomGenericPathRoute is triggered multiple times when the application starts.
To be more specific, I did the following:

Step 1: Route Provider


    public partial class RouteProvider : IRouteProvider
    {
        public void RegisterRoutes(IRouteBuilder routeBuilder)
        {

            routeBuilder.CustomMapGenericPathRoute("CustomRouting", "{GenericSeName}",
              new { controller = "PluginController", action = "CustomRouting" });
        }

        public int Priority => int.MaxValue;
    }


Step 2: Custom GenericPathRoute


    public class CustomGenericPathRoute : GenericPathRoute
    {
        #region Fields

        private readonly IRouter _target;

        #endregion

        #region Ctor

        /// <summary>
        /// Ctor
        /// </summary>
        /// <param name="target">Target</param>
        /// <param name="routeName">Route name</param>
        /// <param name="routeTemplate">Route remplate</param>
        /// <param name="defaults">Defaults</param>
        /// <param name="constraints">Constraints</param>
        /// <param name="dataTokens">Data tokens</param>
        /// <param name="inlineConstraintResolver">Inline constraint resolver</param>
        public CustomGenericPathRoute(IRouter target, string routeName, string routeTemplate, RouteValueDictionary defaults,
            IDictionary<string, object> constraints, RouteValueDictionary dataTokens, IInlineConstraintResolver inlineConstraintResolver)
            : base(target, routeName, routeTemplate, defaults, constraints, dataTokens, inlineConstraintResolver)
        {
            _target = target ?? throw new ArgumentNullException(nameof(target));
        }

        #endregion

        #region Methods

        /// <summary>
        /// Route request to the particular action
        /// </summary>
        /// <param name="context">A route context object</param>
        /// <returns>Task of the routing</returns>
        public override Task RouteAsync(RouteContext context)
        {
            if (!DataSettingsManager.DatabaseIsInstalled)
                return Task.CompletedTask;

            //try to get slug from the route data
            var routeValues = GetRouteValues(context);
            if (!routeValues.TryGetValue("GenericSeName", out object slugValue) || string.IsNullOrEmpty(slugValue as string))
                return Task.CompletedTask;

            var slug = slugValue as string;

            //performance optimization, we load a cached verion here. It reduces number of SQL requests for each page load
            var urlRecordService = EngineContext.Current.Resolve<IUrlRecordService>();
            var urlRecord = urlRecordService.GetBySlugCached(slug);
            //comment the line above and uncomment the line below in order to disable this performance "workaround"
            //var urlRecord = urlRecordService.GetBySlug(slug);

            //no URL record found
            if (urlRecord == null)
                return Task.CompletedTask;

            //virtual directory path
            var pathBase = context.HttpContext.Request.PathBase;

            //if URL record is not active let's find the latest one
            if (!urlRecord.IsActive)
            {
                var activeSlug = urlRecordService.GetActiveSlug(urlRecord.EntityId, urlRecord.EntityName, urlRecord.LanguageId);
                if (string.IsNullOrEmpty(activeSlug))
                    return Task.CompletedTask;

                //redirect to active slug if found
                var redirectionRouteData = new RouteData(context.RouteData);
                redirectionRouteData.Values[NopPathRouteDefaults.ControllerFieldKey] = "Common";
                redirectionRouteData.Values[NopPathRouteDefaults.ActionFieldKey] = "InternalRedirect";
                redirectionRouteData.Values[NopPathRouteDefaults.UrlFieldKey] = $"{pathBase}/{activeSlug}{context.HttpContext.Request.QueryString}";
                redirectionRouteData.Values[NopPathRouteDefaults.PermanentRedirectFieldKey] = true;
                context.HttpContext.Items["nop.RedirectFromGenericPathRoute"] = true;
                context.RouteData = redirectionRouteData;
                return _target.RouteAsync(context);
            }

            //ensure that the slug is the same for the current language,
            //otherwise it can cause some issues when customers choose a new language but a slug stays the same
            var slugForCurrentLanguage = urlRecordService.GetSeName(urlRecord.EntityId, urlRecord.EntityName);
            if (!string.IsNullOrEmpty(slugForCurrentLanguage) && !slugForCurrentLanguage.Equals(slug, StringComparison.InvariantCultureIgnoreCase))
            {
                //we should make validation above because some entities does not have SeName for standard (Id = 0) language (e.g. news, blog posts)

                //redirect to the page for current language
                var redirectionRouteData = new RouteData(context.RouteData);
                redirectionRouteData.Values[NopPathRouteDefaults.ControllerFieldKey] = "Common";
                redirectionRouteData.Values[NopPathRouteDefaults.ActionFieldKey] = "InternalRedirect";
                redirectionRouteData.Values[NopPathRouteDefaults.UrlFieldKey] = $"{pathBase}/{slugForCurrentLanguage}{context.HttpContext.Request.QueryString}";
                redirectionRouteData.Values[NopPathRouteDefaults.PermanentRedirectFieldKey] = false;
                context.HttpContext.Items["nop.RedirectFromGenericPathRoute"] = true;
                context.RouteData = redirectionRouteData;
                return _target.RouteAsync(context);
            }

            //since we are here, all is ok with the slug, so process URL
            var currentRouteData = new RouteData(context.RouteData);
            switch (urlRecord.EntityName.ToLowerInvariant())
            {
                case "product":
                    currentRouteData.Values[NopPathRouteDefaults.ControllerFieldKey] = "Product";
                    currentRouteData.Values[NopPathRouteDefaults.ActionFieldKey] = "ProductDetails";
                    currentRouteData.Values[NopPathRouteDefaults.ProductIdFieldKey] = urlRecord.EntityId;
                    currentRouteData.Values[NopPathRouteDefaults.SeNameFieldKey] = urlRecord.Slug;
                    break;
                case "producttag":
                    currentRouteData.Values[NopPathRouteDefaults.ControllerFieldKey] = "Catalog";
                    currentRouteData.Values[NopPathRouteDefaults.ActionFieldKey] = "ProductsByTag";
                    currentRouteData.Values[NopPathRouteDefaults.ProducttagIdFieldKey] = urlRecord.EntityId;
                    currentRouteData.Values[NopPathRouteDefaults.SeNameFieldKey] = urlRecord.Slug;
                    break;
                case "category":
                    //currentRouteData.Values[NopPathRouteDefaults.ControllerFieldKey] = "Catalog";
                    //currentRouteData.Values[NopPathRouteDefaults.ActionFieldKey] = "Category";
                    currentRouteData.Values[NopPathRouteDefaults.ControllerFieldKey] = "PluginController";
                    currentRouteData.Values[NopPathRouteDefaults.ActionFieldKey] = "CustomRouting";
                    currentRouteData.Values[NopPathRouteDefaults.CategoryIdFieldKey] = urlRecord.EntityId;
                    currentRouteData.Values[NopPathRouteDefaults.SeNameFieldKey] = urlRecord.Slug;
                    break;
                case "manufacturer":
                    currentRouteData.Values[NopPathRouteDefaults.ControllerFieldKey] = "Catalog";
                    currentRouteData.Values[NopPathRouteDefaults.ActionFieldKey] = "Manufacturer";
                    currentRouteData.Values[NopPathRouteDefaults.ManufacturerIdFieldKey] = urlRecord.EntityId;
                    currentRouteData.Values[NopPathRouteDefaults.SeNameFieldKey] = urlRecord.Slug;
                    break;
                case "vendor":
                    currentRouteData.Values[NopPathRouteDefaults.ControllerFieldKey] = "Catalog";
                    currentRouteData.Values[NopPathRouteDefaults.ActionFieldKey] = "Vendor";
                    currentRouteData.Values[NopPathRouteDefaults.VendorIdFieldKey] = urlRecord.EntityId;
                    currentRouteData.Values[NopPathRouteDefaults.SeNameFieldKey] = urlRecord.Slug;
                    break;
                case "newsitem":
                    currentRouteData.Values[NopPathRouteDefaults.ControllerFieldKey] = "News";
                    currentRouteData.Values[NopPathRouteDefaults.ActionFieldKey] = "NewsItem";
                    currentRouteData.Values[NopPathRouteDefaults.NewsItemIdFieldKey] = urlRecord.EntityId;
                    currentRouteData.Values[NopPathRouteDefaults.SeNameFieldKey] = urlRecord.Slug;
                    break;
                case "blogpost":
                    currentRouteData.Values[NopPathRouteDefaults.ControllerFieldKey] = "Blog";
                    currentRouteData.Values[NopPathRouteDefaults.ActionFieldKey] = "BlogPost";
                    currentRouteData.Values[NopPathRouteDefaults.BlogPostIdFieldKey] = urlRecord.EntityId;
                    currentRouteData.Values[NopPathRouteDefaults.SeNameFieldKey] = urlRecord.Slug;
                    break;
                case "topic":
                    currentRouteData.Values[NopPathRouteDefaults.ControllerFieldKey] = "Topic";
                    currentRouteData.Values[NopPathRouteDefaults.ActionFieldKey] = "TopicDetails";
                    currentRouteData.Values[NopPathRouteDefaults.TopicIdFieldKey] = urlRecord.EntityId;
                    currentRouteData.Values[NopPathRouteDefaults.SeNameFieldKey] = urlRecord.Slug;
                    break;
                default:
                    //no record found, thus generate an event this way developers could insert their own types
                    EngineContext.Current.Resolve<IEventPublisher>()
                        ?.Publish(new CustomUrlRecordEntityNameRequestedEvent(currentRouteData, urlRecord));
                    break;
            }
            context.RouteData = currentRouteData;

            //route request
            return _target.RouteAsync(context);
        }

        #endregion
    }


Step 3: Custom GenericPathRouteExtensions


    public static class CustomGenericPathRouteExtensions
    {
        /// <summary>
        /// Adds a route to the route builder with the specified name and template
        /// </summary>
        /// <param name="routeBuilder">The route builder to add the route to</param>
        /// <param name="name">The name of the route</param>
        /// <param name="template">The URL pattern of the route</param>
        /// <returns>Route builder</returns>
        public static IRouteBuilder CustomMapGenericPathRoute(this IRouteBuilder routeBuilder, string name, string template)
        {
            return CustomMapGenericPathRoute(routeBuilder, name, template, defaults: null);
        }

        /// <summary>
        /// Adds a route to the route builder with the specified name, template, and default values
        /// </summary>
        /// <param name="routeBuilder">The route builder to add the route to</param>
        /// <param name="name">The name of the route</param>
        /// <param name="template">The URL pattern of the route</param>
        /// <param name="defaults">An object that contains default values for route parameters.
        /// The object's properties represent the names and values of the default values</param>
        /// <returns>Route builder</returns>
        public static IRouteBuilder CustomMapGenericPathRoute(this IRouteBuilder routeBuilder, string name, string template, object defaults)
        {
            return CustomMapGenericPathRoute(routeBuilder, name, template, defaults, constraints: null);
        }

        /// <summary>
        /// Adds a route to the route builder with the specified name, template, default values, and constraints.
        /// </summary>
        /// <param name="routeBuilder">The route builder to add the route to</param>
        /// <param name="name">The name of the route</param>
        /// <param name="template">The URL pattern of the route</param>
        /// <param name="defaults"> An object that contains default values for route parameters.
        /// The object's properties represent the names and values of the default values</param>
        /// <param name="constraints">An object that contains constraints for the route.
        /// The object's properties represent the names and values of the constraints</param>
        /// <returns>Route builder</returns>
        public static IRouteBuilder CustomMapGenericPathRoute(this IRouteBuilder routeBuilder,
            string name, string template, object defaults, object constraints)
        {
            return CustomMapGenericPathRoute(routeBuilder, name, template, defaults, constraints, dataTokens: null);
        }

        /// <summary>
        /// Adds a route to the route builder with the specified name, template, default values, constraints and data tokens.
        /// </summary>
        /// <param name="routeBuilder">The route builder to add the route to</param>
        /// <param name="name">The name of the route</param>
        /// <param name="template">The URL pattern of the route</param>
        /// <param name="defaults"> An object that contains default values for route parameters.
        /// The object's properties represent the names and values of the default values</param>
        /// <param name="constraints">An object that contains constraints for the route.
        /// The object's properties represent the names and values of the constraints</param>
        /// <param name="dataTokens">An object that contains data tokens for the route.
        /// The object's properties represent the names and values of the data tokens</param>
        /// <returns>Route builder</returns>
        public static IRouteBuilder CustomMapGenericPathRoute(this IRouteBuilder routeBuilder,
            string name, string template, object defaults, object constraints, object dataTokens)
        {
            if (routeBuilder.DefaultHandler == null)
                throw new ArgumentNullException(nameof(routeBuilder));

            //get registered InlineConstraintResolver
            var inlineConstraintResolver = routeBuilder.ServiceProvider.GetRequiredService<IInlineConstraintResolver>();

            //create new generic route
            routeBuilder.Routes.Add(new CustomGenericPathRoute(routeBuilder.DefaultHandler, name, template,
                new RouteValueDictionary(defaults), new RouteValueDictionary(constraints), new RouteValueDictionary(dataTokens),
                inlineConstraintResolver));

            return routeBuilder;
        }
    }


If I comment out the CustomMapGenericPathRoute in RouteProvider, then the RouteAsync method in GenericPathRoute(Nop.Web.Framework) is triggered correctly.
Am I missing something?

I have read a lot of topics in community forum(https://www.nopcommerce.com/boards/t/55647/override-genericrouteproductdetails-from-plugin-for-nopcommerce41-and-40-.aspx , https://www.nopcommerce.com/boards/t/41173/how-to-override-genericpathroutegetroutedata-in-plugin.aspx) but none of them solved my problem.
Thanks in advance.
2 months ago
Hi John,

I know this was a while ago, but did you manage to figure this out with 4.20? I just can't get me CustomMapGenericPathRoute to work :(

Regards,
Craig