Search code examples
c#asp.net-coreasp.net-core-mvcasp.net-core-3.1asp.net-core-localization

.NET MVC Core 3.1 Localization UrlHelper doesn't take the current culture


I'm converting a .Net 4.7 application to .Net Core 3.1. I am updating the localization part. I followed some examples like localization-in-asp-net-core-3-1-mvc.

It works fine but I didn't find a way to make the UrlHelper.Action working without precise the culture. I would like to set automatically the culture parameter. It should come from userclaims, or previous request culture, or default culture.

For example, if the URL is "/Home/Contact", the UrlHelper or HtmlHelper generated a link will be /Home/About. If the current URL is "/en/Home/Contact", the link will be generated as "/en/Home/About". If the user is authenticated it should be "/userCulture/Home/About".

But I can not force my route template only "{culture=en}/{controller=Home}/{action=Welcome}/{id?}" because root url must accessible and the API urls should stay lile "api/somestufff".

Startup.cs :

var supportedCultures = CultureHelper.Cultures.Select(a => new CultureInfo(a)).ToArray();
var requestLocalizationOptions = new RequestLocalizationOptions();
requestLocalizationOptions.SupportedCultures = supportedCultures;
services.AddLocalization();
services.Configure<RequestLocalizationOptions>(options =>
{
    options.DefaultRequestCulture = new RequestCulture("fr", "fr");
    options.SupportedCultures = supportedCultures;
    options.SupportedUICultures = supportedCultures;
    options.RequestCultureProviders.Insert(0, new RouteValueRequestCultureProvider() { Options = requestLocalizationOptions });
});

//......................................

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(name: "culture-route", pattern: "{culture=en}/{controller=Home}/{action=Index}/{id?}");
    endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
});

Controller :

[Route("api/somestufff")]
public async Task<IActionResult> Somestufff()
{ ... }

[Route("{culture:length(2)}/items/{number:int}/{permalink?}")]
public async Task<IActionResult> DisplayItem(int number, string permalink)
{ ... }

Razor Page :

Url.Action("DisplayItem", "MyController",new { culture = ViewBag.Culture as string, number = 123, permalink = "permalink1235" }) 
// => /en/items/123/permalink1235 OK

Url.Action("DisplayItem", "MyController",new { number = 123, permalink = "permalink1235"  }) 
// => /en/MyController/DisplayItem?number=123&permalink=permalink1235 KO

Is there a way to make Urlhelper add the culture from the current context if it's missing?


Solution

  • Since you are in .NET Core, you should replace the Url.Action methods by using Tag helpers.

    By using tag helpers, you can use something like this:

    <a asp-controller="MyController" asp-action="DisplayItem" asp-route-number="123">Click</a>
    

    This obviously doesn't solve the culture in your route, which we will solve in a minute.

    The current tag holder that processes the asp- attributes on the a tag is done by the AnchorTagHelper class in Microsoft.AspNetCore.Mvc.TagHelpers.

    We can inherit that class and add functionality about handling the culture.

    1. Create a class named CultureAnchorTagHelper in your MVC project under /TagHelpers
    2. Fill the content as follows:
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Mvc.TagHelpers;
    using Microsoft.AspNetCore.Mvc.ViewFeatures;
    using Microsoft.AspNetCore.Razor.TagHelpers;
    
    namespace YourMvcName.TagHelpers
    {
        [HtmlTargetElement("a", Attributes = ActionAttributeName)]
        [HtmlTargetElement("a", Attributes = ControllerAttributeName)]
        [HtmlTargetElement("a", Attributes = AreaAttributeName)]
        [HtmlTargetElement("a", Attributes = PageAttributeName)]
        [HtmlTargetElement("a", Attributes = PageHandlerAttributeName)]
        [HtmlTargetElement("a", Attributes = FragmentAttributeName)]
        [HtmlTargetElement("a", Attributes = HostAttributeName)]
        [HtmlTargetElement("a", Attributes = ProtocolAttributeName)]
        [HtmlTargetElement("a", Attributes = RouteAttributeName)]
        [HtmlTargetElement("a", Attributes = RouteValuesDictionaryName)]
        [HtmlTargetElement("a", Attributes = RouteValuesPrefix + "*")]
        public class CultureAnchorTagHelper : AnchorTagHelper
        {
            public CultureAnchorTagHelper(IHttpContextAccessor contextAccessor, IHtmlGenerator generator) :
                base(generator)
            {
                this.contextAccessor = contextAccessor;
            }
    
            private const string ActionAttributeName = "asp-action";
            private const string ControllerAttributeName = "asp-controller";
            private const string AreaAttributeName = "asp-area";
            private const string PageAttributeName = "asp-page";
            private const string PageHandlerAttributeName = "asp-page-handler";
            private const string FragmentAttributeName = "asp-fragment";
            private const string HostAttributeName = "asp-host";
            private const string ProtocolAttributeName = "asp-protocol";
            private const string RouteAttributeName = "asp-route";
            private const string RouteValuesDictionaryName = "asp-all-route-data";
            private const string RouteValuesPrefix = "asp-route-";
            private const string Href = "href";
    
            private readonly IHttpContextAccessor contextAccessor;
            private readonly string defaultRequestCulture = "en";
    
            public override void Process(TagHelperContext context, TagHelperOutput output)
            {
                // Get the culture from the route values
                var culture = (string)contextAccessor.HttpContext.Request.RouteValues["culture"];
     
                // Set the culture in the route values if it is not null
                if (culture != null && culture != defaultRequestCulture)
                {
                    // Remove the 'href' just in case
                    output.Attributes.RemoveAll("href");
                    RouteValues["culture"] = culture;
                }
               
                // Call the base class for all other functionality, we've only added the culture route value.
                // Because the route has the `{culture=en}`, the tag helper knows what to do with that route value.
                base.Process(context, output);
            }
        }
    }
    
    
    1. Modify Shared/_ViewImports.cshtml to remove the current AnchorTagHelper, and use our newly created tag helper as follows:
    @removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.AnchorTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers
    @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
    @addTagHelper *, YourMvcName
    

    With all the changed applied, the culture will be filled in when you use the tag helper.

    a tag in visual studio

    output of a tag