In the last post, we explored a jumpstart guide to working with a user’s request culture. While localization works out of the box, there seems to be a caveat when it comes to remembering the culture a user prefers.

When using the RequestLocalizationMiddleware, we have access to three default providers: Headers, Cookies, and Query String. All of these work as read-only mechanisms, and do not store/remember a user’s language for the duration of their session.

In this post, we’ll see what it takes to persist someone’s culture throughout their visit and the steps required to make it work.

Remove AcceptLanguageHeaderRequestCultureProvider

We first need to remove one of the default providers, AcceptLanguageHeaderRequestCultureProvider, included with RequestLocalizationOptions. This provider is useful if you trust a user’s client, but it can override UI elements that allow our users to pick their culture. In ConfigureServices, we can remove the provider with the following code.

services.Configure<RequestLocalizationOptions>(options =>
{
    options.SetDefaultCulture("en-Us");
    options.AddSupportedUICultures("en-US", "de-DE", "ja-JP");
    options.FallBackToParentUICultures = true;

    options
        .RequestCultureProviders
        .Remove(typeof(AcceptLanguageHeaderRequestCultureProvider));
});

The Remove call, which takes a type, is an extension method.

public static class ListExtensions
{
    public static void Remove<T>(this IList<T> list, Type type)
    {
        var instances = list.Where(x => x.GetType() == type).ToList();
        instances.ForEach(obj => list.Remove(obj));
    }
}

Writing The Cookies Middleware

Like mentioned before, we need a mechanism to persist a user’s selected culture. Middleware is an excellent option as culture is a cross-cutting concern of our application experience.

public static class RequestLocalizationCookiesMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestLocalizationCookies(this IApplicationBuilder app)
    {
        app.UseMiddleware<RequestLocalizationCookiesMiddleware>();
        return app;
    }
} 

public class RequestLocalizationCookiesMiddleware : IMiddleware
{
    public CookieRequestCultureProvider Provider { get; }

    public RequestLocalizationCookiesMiddleware(IOptions<RequestLocalizationOptions> requestLocalizationOptions)
    {
        Provider =
            requestLocalizationOptions
                .Value
                .RequestCultureProviders
                .Where(x => x is CookieRequestCultureProvider)
                .Cast<CookieRequestCultureProvider>()
                .FirstOrDefault();
    }
    
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        if (Provider != null)
        {
            var feature = context.Features.Get<IRequestCultureFeature>();

            if (feature != null)
            {
                // remember culture across request
                context.Response
                    .Cookies
                    .Append(
                        Provider.CookieName,
                        CookieRequestCultureProvider.MakeCookieValue(feature.RequestCulture)
                    );
            }
        }

        await next(context);
    }
}

A few important elements to note about the RequestLocalizationCookiesMiddleware implementation:

  • We inject the RequestLocalizationOptions instance defined in our ConfigureServices. Access to the options is necessary to retrieve the cookie name.
  • We pull the IRequestCultureFeature from the Features collection.
  • We implement an extension method for convenience.

Excellent, all that’s left is to register the middleware.

Wiring Up Our Middleware

In our ConfigureServices method, we need to update our registration of services and types.

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddRazorPages()
        .AddViewLocalization()
        .AddDataAnnotationsLocalization();

    services.AddLocalization(options =>
    {
        options.ResourcesPath = "Resources";
    });

    services.Configure<RequestLocalizationOptions>(options =>
    {
        options.SetDefaultCulture("en-Us");
        options.AddSupportedUICultures("en-US", "de-DE", "ja-JP");
        options.FallBackToParentUICultures = true;

        options
            .RequestCultureProviders
            .Remove(typeof(AcceptLanguageHeaderRequestCultureProvider));
    });
    
    services.AddScoped<RequestLocalizationCookiesMiddleware>();
}

We need to register our middleware so that ASP.NET dependency injection can inject our necessary types. We also add the middleware as scoped to tie any processing to a user’s request.

Finally, we need to use our extension method in our Configure method, making sure to call UseRequestLocalizationCookies after UseRequestLocalization because we need access to the IRequestCultureFeature inside our middleware. Incorrectly registering the two middleware will yield no results.

app.UseRequestLocalization();

// will remember to write the cookie 
app.UseRequestLocalizationCookies();

Conclusion

Starting our application should now remember our culture as we navigate across the site. It makes sense for ASP.NET not to assume how we want to persist our user’s culture, and as we’ve seen from the code above, adding a custom middleware is trivial. Hopefully, this code helps, and please let me know if you found it so.

Also, you can find this code in action on my GitHub repository.

Thanks, and please feel free to leave a comment.