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.