Try your search with a different keyword or use * as a wildcard.
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Nop.Core;
using Nop.Core.Domain.Blogs;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Localization;
using Nop.Core.Domain.News;
using Nop.Core.Domain.Seo;
using Nop.Core.Domain.Topics;
using Nop.Core.Domain.Vendors;
using Nop.Core.Events;
using Nop.Core.Http;
using Nop.Services.Catalog;
using Nop.Services.Localization;
using Nop.Services.Seo;
using Nop.Web.Framework.Events;
namespace Nop.Web.Framework.Mvc.Routing;
///
/// Represents slug route transformer
///
public partial class SlugRouteTransformer : DynamicRouteValueTransformer
{
#region Fields
protected readonly CatalogSettings _catalogSettings;
protected readonly ICategoryService _categoryService;
protected readonly IEventPublisher _eventPublisher;
protected readonly ILanguageService _languageService;
protected readonly IManufacturerService _manufacturerService;
protected readonly IStoreContext _storeContext;
protected readonly IUrlRecordService _urlRecordService;
protected readonly LocalizationSettings _localizationSettings;
#endregion
#region Ctor
public SlugRouteTransformer(CatalogSettings catalogSettings,
ICategoryService categoryService,
IEventPublisher eventPublisher,
ILanguageService languageService,
IManufacturerService manufacturerService,
IStoreContext storeContext,
IUrlRecordService urlRecordService,
LocalizationSettings localizationSettings)
{
_catalogSettings = catalogSettings;
_categoryService = categoryService;
_eventPublisher = eventPublisher;
_languageService = languageService;
_manufacturerService = manufacturerService;
_storeContext = storeContext;
_urlRecordService = urlRecordService;
_localizationSettings = localizationSettings;
}
#endregion
#region Utilities
///
/// Transform route values according to the passed URL record
///
/// HTTP context
/// The route values associated with the current match
/// Record found by the URL slug
/// URL catalog path
/// A task that represents the asynchronous operation
protected virtual async Task SingleSlugRoutingAsync(HttpContext httpContext, RouteValueDictionary values, UrlRecord urlRecord, string catalogPath)
{
//if URL record is not active let's find the latest one
var slug = urlRecord.IsActive
? urlRecord.Slug
: await _urlRecordService.GetActiveSlugAsync(urlRecord.EntityId, urlRecord.EntityName, urlRecord.LanguageId);
if (string.IsNullOrEmpty(slug))
return;
if (!urlRecord.IsActive || !string.IsNullOrEmpty(catalogPath))
{
//permanent redirect to new URL with active single slug
InternalRedirect(httpContext, values, $"/{slug}", true);
return;
}
//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
if (_localizationSettings.SeoFriendlyUrlsForLanguagesEnabled && values.TryGetValue(NopRoutingDefaults.RouteValue.Language, out var langValue))
{
var store = await _storeContext.GetCurrentStoreAsync();
var languages = await _languageService.GetAllLanguagesAsync(storeId: store.Id);
var language = languages
.FirstOrDefault(lang => lang.Published && lang.UniqueSeoCode.Equals(langValue?.ToString(), StringComparison.InvariantCultureIgnoreCase))
?? languages.FirstOrDefault();
var slugLocalized = await _urlRecordService.GetActiveSlugAsync(urlRecord.EntityId, urlRecord.EntityName, language.Id);
if (!string.IsNullOrEmpty(slugLocalized) && !slugLocalized.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
InternalRedirect(httpContext, values, $"/{language.UniqueSeoCode}/{slugLocalized}", false);
return;
}
}
//since we are here, all is ok with the slug, so process URL
switch (urlRecord.EntityName)
{
case var name when name.Equals(nameof(Product), StringComparison.InvariantCultureIgnoreCase):
RouteToAction(values, "Product", "ProductDetails", slug, (NopRoutingDefaults.RouteValue.ProductId, urlRecord.EntityId));
return;
case var name when name.Equals(nameof(ProductTag), StringComparison.InvariantCultureIgnoreCase):
RouteToAction(values, "Catalog", "ProductsByTag", slug, (NopRoutingDefaults.RouteValue.ProductTagId, urlRecord.EntityId));
return;
case var name when name.Equals(nameof(Category), StringComparison.InvariantCultureIgnoreCase):
RouteToAction(values, "Catalog", "Category", slug, (NopRoutingDefaults.RouteValue.CategoryId, urlRecord.EntityId));
return;
case var name when name.Equals(nameof(Manufacturer), StringComparison.InvariantCultureIgnoreCase):
RouteToAction(values, "Catalog", "Manufacturer", slug, (NopRoutingDefaults.RouteValue.ManufacturerId, urlRecord.EntityId));
return;
case var name when name.Equals(nameof(Vendor), StringComparison.InvariantCultureIgnoreCase):
RouteToAction(values, "Catalog", "Vendor", slug, (NopRoutingDefaults.RouteValue.VendorId, urlRecord.EntityId));
return;
case var name when name.Equals(nameof(NewsItem), StringComparison.InvariantCultureIgnoreCase):
RouteToAction(values, "News", "NewsItem", slug, (NopRoutingDefaults.RouteValue.NewsItemId, urlRecord.EntityId));
return;
case var name when name.Equals(nameof(BlogPost), StringComparison.InvariantCultureIgnoreCase):
RouteToAction(values, "Blog", "BlogPost", slug, (NopRoutingDefaults.RouteValue.BlogPostId, urlRecord.EntityId));
return;
case var name when name.Equals(nameof(Topic), StringComparison.InvariantCultureIgnoreCase):
RouteToAction(values, "Topic", "TopicDetails", slug, (NopRoutingDefaults.RouteValue.TopicId, urlRecord.EntityId));
return;
}
}
///
/// Try transforming the route values, assuming the passed URL record is of a product type
///
/// HTTP context
/// The route values associated with the current match
/// Record found by the URL slug
/// URL catalog path
///
/// A task that represents the asynchronous operation
/// The task result contains a value whether the route values were processed
///
protected virtual async Task TryProductCatalogRoutingAsync(HttpContext httpContext, RouteValueDictionary values, UrlRecord urlRecord, string catalogPath)
{
//ensure it's a product URL record
if (!urlRecord.EntityName.Equals(nameof(Product), StringComparison.InvariantCultureIgnoreCase))
return false;
//if the product URL structure type is product seName only, it will be processed later by a single slug
if (_catalogSettings.ProductUrlStructureTypeId == (int)ProductUrlStructureType.Product)
return false;
//get active slug for the product
var slug = urlRecord.IsActive
? urlRecord.Slug
: await _urlRecordService.GetActiveSlugAsync(urlRecord.EntityId, urlRecord.EntityName, urlRecord.LanguageId);
if (string.IsNullOrEmpty(slug))
return false;
//try to get active catalog (e.g. category or manufacturer) seName for the product
var catalogSeName = string.Empty;
var isCategoryProductUrl = _catalogSettings.ProductUrlStructureTypeId == (int)ProductUrlStructureType.CategoryProduct;
if (isCategoryProductUrl)
{
var productCategory = (await _categoryService.GetProductCategoriesByProductIdAsync(urlRecord.EntityId)).FirstOrDefault();
var category = await _categoryService.GetCategoryByIdAsync(productCategory?.CategoryId ?? 0);
catalogSeName = category is not null ? await _urlRecordService.GetSeNameAsync(category) : string.Empty;
}
var isManufacturerProductUrl = _catalogSettings.ProductUrlStructureTypeId == (int)ProductUrlStructureType.ManufacturerProduct;
if (isManufacturerProductUrl)
{
var productManufacturer = (await _manufacturerService.GetProductManufacturersByProductIdAsync(urlRecord.EntityId)).FirstOrDefault();
var manufacturer = await _manufacturerService.GetManufacturerByIdAsync(productManufacturer?.ManufacturerId ?? 0);
catalogSeName = manufacturer is not null ? await _urlRecordService.GetSeNameAsync(manufacturer) : string.Empty;
}
if (string.IsNullOrEmpty(catalogSeName))
return false;
//get URL record by the specified catalog path
var catalogUrlRecord = await _urlRecordService.GetBySlugAsync(catalogPath);
if (catalogUrlRecord is null ||
(isCategoryProductUrl && !catalogUrlRecord.EntityName.Equals(nameof(Category), StringComparison.InvariantCultureIgnoreCase)) ||
(isManufacturerProductUrl && !catalogUrlRecord.EntityName.Equals(nameof(Manufacturer), StringComparison.InvariantCultureIgnoreCase)) ||
!urlRecord.IsActive)
{
//permanent redirect to new URL with active catalog seName and active slug
InternalRedirect(httpContext, values, $"/{catalogSeName}/{slug}", true);
return true;
}
//ensure the catalog seName and slug are the same for the current language
if (_localizationSettings.SeoFriendlyUrlsForLanguagesEnabled && values.TryGetValue(NopRoutingDefaults.RouteValue.Language, out var langValue))
{
var store = await _storeContext.GetCurrentStoreAsync();
var languages = await _languageService.GetAllLanguagesAsync(storeId: store.Id);
var language = languages
.FirstOrDefault(lang => lang.Published && lang.UniqueSeoCode.Equals(langValue?.ToString(), StringComparison.InvariantCultureIgnoreCase))
?? languages.FirstOrDefault();
var slugLocalized = await _urlRecordService.GetActiveSlugAsync(urlRecord.EntityId, urlRecord.EntityName, language.Id);
var catalogSlugLocalized = await _urlRecordService.GetActiveSlugAsync(catalogUrlRecord.EntityId, catalogUrlRecord.EntityName, language.Id);
if ((!string.IsNullOrEmpty(slugLocalized) && !slugLocalized.Equals(slug, StringComparison.InvariantCultureIgnoreCase)) ||
(!string.IsNullOrEmpty(catalogSlugLocalized) && !catalogSlugLocalized.Equals(catalogUrlRecord.Slug, StringComparison.InvariantCultureIgnoreCase)))
{
//redirect to localized URL for the current language
var activeSlug = !string.IsNullOrEmpty(slugLocalized) ? slugLocalized : slug;
var activeCatalogSlug = !string.IsNullOrEmpty(catalogSlugLocalized) ? catalogSlugLocalized : catalogUrlRecord.Slug;
InternalRedirect(httpContext, values, $"/{language.UniqueSeoCode}/{activeCatalogSlug}/{activeSlug}", false);
return true;
}
}
//ensure the specified catalog path is equal to the active catalog seName
//we do it here after localization check to avoid double redirect
if (!catalogSeName.Equals(catalogUrlRecord.Slug, StringComparison.InvariantCultureIgnoreCase))
{
//permanent redirect to new URL with active catalog seName and active slug
InternalRedirect(httpContext, values, $"/{catalogSeName}/{slug}", true);
return true;
}
//all is ok, so select the appropriate action
RouteToAction(values, "Product", "ProductDetails", slug,
(NopRoutingDefaults.RouteValue.ProductId, urlRecord.EntityId), (NopRoutingDefaults.RouteValue.CatalogSeName, catalogSeName));
return true;
}
///
/// Transform route values to redirect the request
///
/// HTTP context
/// The route values associated with the current match
/// Path
/// Whether the redirect should be permanent
protected virtual void InternalRedirect(HttpContext httpContext, RouteValueDictionary values, string path, bool permanent)
{
values[NopRoutingDefaults.RouteValue.Controller] = "Common";
values[NopRoutingDefaults.RouteValue.Action] = "InternalRedirect";
values[NopRoutingDefaults.RouteValue.Url] = $"{httpContext.Request.PathBase}{path}{httpContext.Request.QueryString}";
values[NopRoutingDefaults.RouteValue.PermanentRedirect] = permanent;
httpContext.Items[NopHttpDefaults.GenericRouteInternalRedirect] = true;
}
///
/// Transform route values to set controller, action and action parameters
///
/// The route values associated with the current match
/// Controller name
/// Action name
/// URL slug
/// Action parameters
protected virtual void RouteToAction(RouteValueDictionary values, string controller, string action, string slug, params (string Key, object Value)[] parameters)
{
values[NopRoutingDefaults.RouteValue.Controller] = controller;
values[NopRoutingDefaults.RouteValue.Action] = action;
values[NopRoutingDefaults.RouteValue.SeName] = slug;
foreach (var (key, value) in parameters)
{
values[key] = value;
}
}
#endregion
#region Methods
///
/// Create a set of transformed route values that will be used to select an action
///
/// HTTP context
/// The route values associated with the current match
///
/// A task that represents the asynchronous operation
/// The task result contains the set of values
///
public override async ValueTask TransformAsync(HttpContext httpContext, RouteValueDictionary routeValues)
{
//get values to transform for action selection
var values = new RouteValueDictionary(routeValues);
if (values is null)
return values;
if (!values.TryGetValue(NopRoutingDefaults.RouteValue.SeName, out var slug))
return values;
//find record by the URL slug
if (await _urlRecordService.GetBySlugAsync(slug.ToString()) is not UrlRecord urlRecord)
return values;
//allow third-party handlers to select an action by the found URL record
var routingEvent = new GenericRoutingEvent(httpContext, values, urlRecord);
await _eventPublisher.PublishAsync(routingEvent);
if (routingEvent.Handled)
return values;
//then try to select an action by the found URL record and the catalog path
var catalogPath = values.TryGetValue(NopRoutingDefaults.RouteValue.CatalogSeName, out var catalogPathValue)
? catalogPathValue.ToString()
: string.Empty;
if (await TryProductCatalogRoutingAsync(httpContext, values, urlRecord, catalogPath))
return values;
//finally, select an action by the URL record only
await SingleSlugRoutingAsync(httpContext, values, urlRecord, catalogPath);
return values;
}
#endregion
}