Custom URL for market/language combination

Vote:
 

Situation: a multi lingual, multi market Commerce site. Currently we're using IP lookup to determine the market, but want to change that to include the market in the URL.

We would like to use /language-market/content/bla as a URL (so language and market combined in one url segment). Are there any pointers on how to implement this?

#200120
Jan 02, 2019 11:48
Vote:
 

You can implement your own commerce router, there's examples of working with the HierarchicalCatalogPartialRouter in the Commerce Guru Qaun's book https://leanpub.com/epicommercerecipes/read_sample. There's examples of removing a top level node but if you follow the guides you should be able ad a node in based upon market.

#200122
Jan 02, 2019 14:13
Vote:
 

I don't want to add a node, but combine the market-language into one segment. I just found https://github.com/marisks/examples/blob/master/MarketRouting/Quicksilver/Sources/EPiServer.Reference.Commerce.Site/Features/Market/Routing/RouteCollectionExtensions.cs where it's adding a segment for the market in the URL (after the language segment) but to combine it i believe i have to override the default EPiServer.Web.Routing.Segments.Internal.LanguageSegmentMatcher and add a route to combine the language and market into one segement.

#200133
Jan 03, 2019 9:18
Vote:
 

This is what i came up with for now based on https://github.com/marisks/examples/blob/master/MarketRouting/Quicksilver/Sources/EPiServer.Reference.Commerce.Site/Features/Market/Routing/RouteCollectionExtensions.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Routing;
using EPiServer.Configuration;
using EPiServer.Core;
using EPiServer.Globalization.Internal;
using EPiServer.ServiceLocation;
using EPiServer.Web;
using EPiServer.Web.Mvc;
using EPiServer.Web.Mvc.Internal;
using EPiServer.Web.Routing;
using EPiServer.Web.Routing.Internal;
using EPiServer.Web.Routing.Segments;
using EPiServer.Web.Routing.Segments.Internal;
using Mediachase.Commerce;
using Mediachase.Commerce.Markets;

namespace EPiServer.Reference.Commerce.Site.Features.Market.Routing
{
    public static class RouteCollectionExtensions
    {
        // ReSharper disable UnassignedGetOnlyAutoProperty
        private static Injected<IMarketService> InjectedMarketService { get; }
        private static IMarketService MarketService => InjectedMarketService.Service;

        private static Injected<ICurrentMarket> InjectedCurrentMarket { get; }
        private static ICurrentMarket CurrentMarket => InjectedCurrentMarket.Service;
        // ReSharper restore UnassignedGetOnlyAutoProperty

        public static void MapMarketSegment(this RouteCollection routes)
        {
            var parameters = GetMapContentRouteParameters();

            var segment = new LanguageAndMarket(
                new LanguageSegmentMatcher(
                    parameters.LanguageBranchRepository,
                    ServiceLocator.Current.GetInstance<HostLanguageResolver>()),
                ServiceLocator.Current.GetInstance<HostLanguageResolver>(),
                new VirtualPathHostResolver(
                    parameters.BasePathResolver,
                    ServiceLocator.Current.GetInstance<ServiceAccessor<SiteDefinition>>(),
                    ServiceLocator.Current.GetInstance<ISiteDefinitionRepository>()),
                ServiceLocator.Current.GetInstance<HostNameResolver>(),
                MarketService, 
                CurrentMarket);
            
            if (parameters.SegmentMappings == null)
            {
                parameters.SegmentMappings = new Dictionary<string, ISegment>();
            }
            parameters.SegmentMappings.Add(LanguageAndMarket.SegmentName, segment);

            routes.InsertAndMapContentRoute(
                index: routes.IndexOf("pages"),
                name: LanguageAndMarket.SegmentName,
                url: "{" + LanguageAndMarket.SegmentName + "}/{node}/{partial}/{action}",
                defaults: new { action = "index" },
                parameters: parameters);
        }

        private static MapContentRouteParameters GetMapContentRouteParameters()
        {
            var parameters = new MapContentRouteParameters
            {
                Direction = SupportedDirection.Both
            };

            parameters.ServiceLocator = parameters.ServiceLocator ?? ServiceLocator.Current;
            var routeHandler = parameters.RouteHandler;
            if (parameters.ServiceLocator.AssignNullService(ref routeHandler))
                parameters.RouteHandler = routeHandler;
            var urlSegmentRouter = parameters.UrlSegmentRouter;
            if (parameters.ServiceLocator.AssignNullService(ref urlSegmentRouter))
                parameters.UrlSegmentRouter = urlSegmentRouter;
            var routeParser = parameters.RouteParser;
            if (parameters.ServiceLocator.AssignNullService(ref routeParser))
                parameters.RouteParser = routeParser;
            var branchRepository = parameters.LanguageBranchRepository;
            if (parameters.ServiceLocator.AssignNullService(ref branchRepository))
                parameters.LanguageBranchRepository = branchRepository;
            var viewRegistrator = parameters.ViewRegistrator;
            if (parameters.ServiceLocator.AssignNullService(ref viewRegistrator))
                parameters.ViewRegistrator = viewRegistrator;
            var contentLoader = parameters.ContentLoader;
            if (parameters.ServiceLocator.AssignNullService(ref contentLoader))
                parameters.ContentLoader = contentLoader;
            var partialRouteHandler = parameters.PartialRouteHandler;
            if (parameters.ServiceLocator.AssignNullService(ref partialRouteHandler))
                parameters.PartialRouteHandler = partialRouteHandler;
            var templateResolver = parameters.TemplateResolver;
            if (parameters.ServiceLocator.AssignNullService(ref templateResolver))
                parameters.TemplateResolver = templateResolver;
            var permanentLinkMapper = parameters.PermanentLinkMapper;
            if (parameters.ServiceLocator.AssignNullService(ref permanentLinkMapper))
                parameters.PermanentLinkMapper = permanentLinkMapper;
            var urlResolver = parameters.UrlResolver;
            if (parameters.ServiceLocator.AssignNullService(ref urlResolver))
                parameters.UrlResolver = urlResolver;
            var versionRepository = parameters.ContentVersionRepository;
            if (parameters.ServiceLocator.AssignNullService(ref versionRepository))
                parameters.ContentVersionRepository = versionRepository;
            var updateCurrentLanguage = parameters.UpdateCurrentLanguage;
            if (parameters.ServiceLocator.AssignNullService(ref updateCurrentLanguage))
                parameters.UpdateCurrentLanguage = updateCurrentLanguage;
            if (parameters.BasePathResolver == null)
            {
                var instance = parameters.ServiceLocator.GetInstance<IBasePathResolver>();
                parameters.BasePathResolver = instance.Resolve;
            }
            if (parameters.StrictLanguageRoutingResolver == null)
                parameters.StrictLanguageRoutingResolver = () => Settings.Instance.StrictLanguageRouting;

            return parameters;
        }

        public static IContentRoute InsertAndMapContentRoute(this RouteCollection routes, int index, string name, string url, object defaults, MapContentRouteParameters parameters)
        {
            var dictionary = new Dictionary<string, ISegment>
            {
                {
                    RoutingConstants.NodeKey,
                    new NodeSegment(
                        RoutingConstants.NodeKey,
                        UrlRewriteProvider.FriendlyUrlExtension,
                        parameters.UrlSegmentRouter,
                        parameters.ContentLoader,
                        parameters.UrlResolver,
                        ServiceLocator.Current.GetInstance<IContentLanguageSettingsHandler>())
                },
                {
                    "partial",
                    new PartialSegment("partial", parameters.ContentLoader, parameters.PartialRouteHandler)
                },
                {
                    RoutingConstants.LanguageKey,
                    new LanguageSegment(
                        RoutingConstants.LanguageKey,
                        new LanguageSegmentMatcher(
                            parameters.LanguageBranchRepository,
                            ServiceLocator.Current.GetInstance<HostLanguageResolver>()),
                        ServiceLocator.Current.GetInstance<HostLanguageResolver>(),
                        new VirtualPathHostResolver(
                            parameters.BasePathResolver,
                            ServiceLocator.Current.GetInstance<ServiceAccessor<SiteDefinition>>(),
                            ServiceLocator.Current.GetInstance<ISiteDefinitionRepository>()),
                        ServiceLocator.Current.GetInstance<HostNameResolver>())
                }
            };
            if (parameters.SegmentMappings != null)
            {
                foreach (var segmentMapping in parameters.SegmentMappings)
                    dictionary[segmentMapping.Key] = segmentMapping.Value;
            }
            var constraints = new RouteValueDictionary(parameters.Constraints);
            if (!constraints.ContainsKey(RoutingConstants.ActionKey))
            {
                var controllerTypeMap = parameters.ControllerTypeMap;
                parameters.ServiceLocator.AssignNullService(ref controllerTypeMap);
                parameters.ActionHandlers = parameters.ActionHandlers ?? parameters.ServiceLocator.GetAllInstances<IUnknownActionHandler>().ToArray();
                constraints[RoutingConstants.ActionKey] = new ExistingActionRouteConstraint(parameters);
            }
            var urlSegments = parameters.RouteParser.Parse(url, dictionary);
            var defaultContentRoute1 = new DefaultContentRoute(
                parameters.RouteHandler,
                urlSegments,
                new RouteValueDictionary(defaults),
                constraints,
                parameters.Direction,
                parameters.BasePathResolver,
                parameters.ViewRegistrator,
                parameters.UpdateCurrentLanguage,
                parameters.ServiceLocator.GetInstance<RouteRedirector>(),
                new VirtualPathHostResolver(
                    parameters.BasePathResolver,
                    ServiceLocator.Current.GetInstance<ServiceAccessor<SiteDefinition>>(),
                    ServiceLocator.Current.GetInstance<ISiteDefinitionRepository>()),
                ServiceLocator.Current.GetInstance<IContentRouteEventsRaiser>(),
                ServiceLocator.Current.GetInstance<ServiceAccessor<RoutingOptions>>())
            { Name = name };
            var languageRoutingResolver = parameters.StrictLanguageRoutingResolver;
            defaultContentRoute1.StrictLanguageRoutingResolver = languageRoutingResolver;
            if (routes[name] != null)
                routes.Remove(routes[name]);
            routes.Insert(index, defaultContentRoute1);
            return defaultContentRoute1;
        }

        public static int IndexOf(this RouteCollection routes, string name)
        {
            var defaultRoute = routes
                .Select(r => r as DefaultContentRoute)
                .Where(x => x != null)
                .First(x => x.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
            return routes.IndexOf(defaultRoute);
        }
    }
}

and

using EPiServer.Core;
using EPiServer.Globalization.Internal;
using EPiServer.Logging;
using EPiServer.Web;
using EPiServer.Web.Routing;
using EPiServer.Web.Routing.Internal;
using EPiServer.Web.Routing.Segments;
using Mediachase.Commerce;
using Mediachase.Commerce.Markets;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Web.Routing;

namespace EPiServer.Reference.Commerce.Site.Features.Market.Routing
{
    //Large part copied from EPiServer.Web.Routing.Segments.Internal.LanguageSegment in order to integratie market and language into one segement
    public class LanguageAndMarket : SegmentBase
    {
        private static readonly ILogger _log = LogManager.GetLogger();

        private readonly IMarketService _marketService;
        private readonly ICurrentMarket _currentMarket;
        private readonly ILanguageSegmentMatcher _languageSegmentMatcher;
        private readonly VirtualPathHostResolver _virtualPathHostResolver;
        private readonly HostLanguageResolver _hostLanguageResolver;
        private readonly HostNameResolver _hostNameResolver;

        private IDictionary<string, CultureInfo> _enabledLanguages;

        public IDictionary<string, CultureInfo> EnabledLanguages
        {
            get
            {
                return this._enabledLanguages ?? UrlRewriteContext.Languages;
            }
            set
            {
                this._enabledLanguages = value;
            }
        }

        private bool SingleLanguageSite
        {
            get
            {
                return this.EnabledLanguages.Count == 1;
            }
        }


        public const string SegmentName = "language-market";

        public LanguageAndMarket(ILanguageSegmentMatcher languageSegmentMatcher, HostLanguageResolver hostLanguageResolver, VirtualPathHostResolver virtualPathHostResolver, HostNameResolver hostNameResolver, IMarketService marketService, ICurrentMarket currentMarket)
            : base(SegmentName)
        {
            if (marketService == null) throw new ArgumentNullException(nameof(marketService));
            if (currentMarket == null) throw new ArgumentNullException(nameof(currentMarket));
            _marketService = marketService;
            _currentMarket = currentMarket;

            _languageSegmentMatcher = languageSegmentMatcher;
            _virtualPathHostResolver = virtualPathHostResolver;
            _hostLanguageResolver = hostLanguageResolver;
            _hostNameResolver = hostNameResolver;
        }

        public override bool RouteDataMatch(SegmentContext context)
        {
            string hostName = this._hostNameResolver.Get(context.RequestUrl);
            var segmentPair = context.GetNextValue(context.RemainingPath);

            if (segmentPair.Next.Contains("-"))
            {
                string text;
                if (this._languageSegmentMatcher.TryGetLanguageId(segmentPair.Next.Split('-')[0], out text))
                {
                    if (context.StrictLanguageRoutingResolver())
                    {
                        this.HandleLanguageSegmentStrict(context, hostName, segmentPair, text);
                    }
                    else
                    {
                        context.RemainingPath = segmentPair.Remaining;
                    }
                }
                else
                {
                    text = (this._hostLanguageResolver.GetLanguageForHost(hostName, true) ?? (context.Defaults[RoutingConstants.LanguageKey] as string));
                    if (context.StrictLanguageRoutingResolver() && !this.SingleLanguageSite && string.IsNullOrEmpty(text) && !string.IsNullOrEmpty(context.RemainingPath))
                    {
                        return this.HandleNoLanguageFoundStrict(context);
                    }
                }
                context.Language = text;

                var marketCode = segmentPair.Next.Split('-')[1];

                if (!string.IsNullOrEmpty(marketCode))
                {
                    return ProcessSegment(context, segmentPair);
                }

                if (context.Defaults.ContainsKey(Name))
                {
                    context.RouteData.Values[Name] = context.Defaults[Name];
                    return true;
                }
            }

            return false;
        }

        public override string GetVirtualPathSegment(RequestContext requestContext, RouteValueDictionary values)
        {
            var languageSegment = requestContext.GetRouteValue(RoutingConstants.LanguageKey, values) as string;

            if (languageSegment == null)
            {
                return null;
            }

            CultureInfo language;
            if (!TryGetCultureInfo(languageSegment.Split('-')[0], out language))
            {
                return null;
            }

            string languageCode;
            if (!this._languageSegmentMatcher.TryGetLanguageUrlSegment(language.Name, out languageCode))
            {
                return string.Empty;
            }

            var contentLink = requestContext.GetRouteValue("node", values) as ContentReference;
            if (ContentReference.IsNullOrEmpty(contentLink)) // Skips for non-content items.
            {
                return null;
            }

            var currentMarket = _currentMarket.GetCurrentMarket();
            return $"{languageCode}-{currentMarket.MarketId.Value.ToLower()}";
        }

        private bool ProcessSegment(SegmentContext context, SegmentPair segmentPair)
        {
            var marketCode = segmentPair.Next.Split('-')[1];
            var marketId = new MarketId(marketCode);
            var market = _marketService.GetMarket(marketId);
            if (market == null) return false;

            context.RouteData.Values[Name] = marketCode;
            context.RemainingPath = segmentPair.Remaining;

            _currentMarket.SetCurrentMarket(marketId);

            return true;
        }

        private void HandleLanguageSegmentStrict(SegmentContext context, string hostName, SegmentPair urlSegments, string languageId)
        {
            string languageForHost = this._hostLanguageResolver.GetLanguageForHost(hostName, true);
            if (string.IsNullOrEmpty(languageForHost))
            {
                if (!this.SingleLanguageSite)
                {
                    context.RemainingPath = urlSegments.Remaining;
                    return;
                }
                if (_log.IsDebugEnabled())
                {
                    _log.Debug("Due to strict language routing language was segment '{0}' not consumed since there is only one language enabled '{1}' and hence no language segment should be used", new object[]
                    {
                        urlSegments.Next,
                        languageId
                    });
                    return;
                }
            }
            else
            {
                if (!languageId.Equals(languageForHost, StringComparison.OrdinalIgnoreCase))
                {
                    context.RemainingPath = urlSegments.Remaining;
                    return;
                }
                if (_log.IsDebugEnabled())
                {
                    _log.Debug("Due to strict language routing language was segment '{0}' not consumed since there is a host mapping for language '{1}'", new object[]
                    {
                        urlSegments.Next,
                        languageId
                    });
                }
            }
        }

        protected virtual bool HandleNoLanguageFoundStrict(SegmentContext context)
        {
            if (_log.IsDebugEnabled())
            {
                _log.Debug("There was no language segment found for url '{0}' and no language host mapping found therefore routing is stopped due to strict routing setting", new object[]
                {
                    context.RemainingPath
                });
            }
            return false;
        }

        private static bool TryGetCultureInfo(string cultureCode, out CultureInfo culture)
        {
            if (string.IsNullOrEmpty(cultureCode))
            {
                culture = null;
                return false;
            }
            bool result;
            try
            {
                culture = CultureInfo.GetCultureInfo(cultureCode);
                result = true;
            }
            catch (CultureNotFoundException)
            {
                culture = null;
                result = false;
            }
            return result;
        }


    }
}

the downside is having copy-pasted and modified all code from EPiServer.Web.Routing.Segments.Internal.LanguageSegment in order to get it to work which is not really what you'd want. However i couldn't see any other way. Any suggestions for this?

#200151
Edited, Jan 03, 2019 16:27
Vote:
 

Sadly this is something I've come up against with some before with internal classes. If you raise this issue to Episerver they may look at moving some of logic out in to non internal classes but I think for the time being you'll have to go with what you have got. I've currently got a similar issue with Owin Azure AD authentication and roles providers that pending a change. At least you managed to do it :-)

#200152
Jan 03, 2019 16:43
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.