Search code examples
asp.netasp.net-corerazor.net-core

.Net Core 3.1 Razor Pages : autoredirect to culture


I try to accomplish a similar behaviour with MS Docs.

For example, if you visit https://learn.microsoft.com/, you will be redirected to your culture, in my case I'm being redirected automatically to https://learn.microsoft.com/en-gb/.

Same goes for inner pages if you access them without the culture in the URL.

For instance, by accessing:

https://learn.microsoft.com/aspnet/core/razor-pages/?view=aspnetcore-3.1&tabs=visual-studio

it will be automatically redirect you to:

https://learn.microsoft.com/en-gb/aspnet/core/razor-pages/?view=aspnetcore-3.1&tabs=visual-studio

I have a small demo app where I conduct my localisation experiment for .NET Core 3.1 and Razor Pages here.

I have set options.Conventions here, and I have created CustomCultureRouteRouteModelConvention class here, but I'm fairly novice with .NET Core and I'm kind of stuck on how to implement the above-described functionality.

Thank you all in advance!


Solution

  • You should use existing Rewriting Middleware to do redirects: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/url-rewriting?view=aspnetcore-3.1 In the simplest form, you can tell rewrite middleware to redirect if it does not see a locale pattern at the beginning of the URL path, maybe

    new RewriteOptions() .AddRedirect("^([a-z]{2}-[a-z]{2})", "en-US/$1")

    (regex not tested) or do full redirect class with more detailed rules when and to what locale you want to redirect. Example in that aspnet document references RedirectImageRequest which you can use to get an understanding of how custom redirect rules works. Adapting to your case as a proof of concept, I reused most of the logic in your existing RedirectUnsupportedCulture:

    public class RedirectUnsupportedCultures : IRule
        {
            private readonly string _extension;
            private readonly PathString _newPath;
            private IList<CultureInfo> _cultureItems;
            private string _cultureRouteKey;
    
            public RedirectUnsupportedCultures(IOptions<RequestLocalizationOptions> options)
            {
                RouteDataRequestCultureProvider provider = options.Value.RequestCultureProviders
                    .OfType<RouteDataRequestCultureProvider>()
                    .First();
    
                _cultureItems = options.Value.SupportedUICultures;
    
                _cultureRouteKey = provider.RouteDataStringKey;
            }
    
            public void ApplyRule(RewriteContext rewriteContext)
            {
                // do not redirect static assets and do not redirect from a controller that is meant to set the locale
                // similar to how you would not restrict a guest user from login form on public site.
                if (rewriteContext.HttpContext.Request.Path.Value.EndsWith(".ico") ||
                    rewriteContext.HttpContext.Request.Path.Value.Contains("change-culture"))
                {
                    return;
                }
    
                IRequestCultureFeature cultureFeature = rewriteContext.HttpContext.Features.Get<IRequestCultureFeature>();
                string actualCulture = cultureFeature?.RequestCulture.Culture.Name;
                string requestedCulture = rewriteContext.HttpContext.GetRouteValue(_cultureRouteKey)?.ToString();
    
                // Here you can add more rules to redirect based on maybe cookie setting, or even language options saved in database user profile
                if(string.IsNullOrEmpty(requestedCulture) || _cultureItems.All(x => x.Name != requestedCulture)
                    && !string.Equals(requestedCulture, actualCulture, StringComparison.OrdinalIgnoreCase))
                {
                    string localizedPath = $"/{actualCulture}{rewriteContext.HttpContext.Request.Path.Value}";
    
                    HttpResponse response = rewriteContext.HttpContext.Response;
                    response.StatusCode = StatusCodes.Status301MovedPermanently;
                    rewriteContext.Result = RuleResult.EndResponse;
    
                    // preserve query part parameters of the URL (?parameters) if there were any
                    response.Headers[HeaderNames.Location] =
                        localizedPath + rewriteContext.HttpContext.Request.QueryString;
                }
            }
    

    and registered it in Startup.cs with

    // Attempt to make auto-redirect to culture if it is not exist in the url
    RewriteOptions rewriter = new RewriteOptions();
    rewriter.Add(new RedirectUnsupportedCultures(app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>()));
    app.UseRewriter(rewriter);
    

    Improvement:

    After using the above code I bumped on a bug that in case the culture is not supported by the application, the redirection will end up with infinite culture paths. For example, if I support the cultures en (default) and gr, if instead of either /en/foobar or /gr/foobar I would write /fr/foobar, I would end up getting /en/fr/foobar then /en/en/fr/foobar and etc.

    I added private readonly LinkGenerator _linkGenerator; to the class, which I initialise it in the constructor. I removed that line string localizedPath = $"/{actualCulture}{rewriteContext.HttpContext.Request.Path.Value}"; and the code after that line looks like this:

    rewriteContext.HttpContext.GetRouteData().Values[_cultureRouteKey] = actualCulture;
    
    HttpResponse response = rewriteContext.HttpContext.Response;
    response.StatusCode   = StatusCodes.Status301MovedPermanently;
    rewriteContext.Result = RuleResult.EndResponse;
    
    // preserve query part parameters of the URL (?parameters) if there were any
    response.Headers[HeaderNames.Location] =
        _linkGenerator.GetPathByAction(
            rewriteContext.HttpContext,
            values: rewriteContext.HttpContext.GetRouteData().Values
        )
      + rewriteContext.HttpContext.Request.QueryString;