Optionally override request culture via url/route in an ASP.NET Core 1.0 Application

Viewed 11143

I am trying to override the culture of the current request. I got it working partly using a custom ActionFilterAttribute.

public sealed class LanguageActionFilter : ActionFilterAttribute
{
    private readonly ILogger logger;
    private readonly IOptions<RequestLocalizationOptions> localizationOptions;

    public LanguageActionFilter(ILoggerFactory loggerFactory, IOptions<RequestLocalizationOptions> options)
    {
        if (loggerFactory == null)
            throw new ArgumentNullException(nameof(loggerFactory));

        if (options == null)
            throw new ArgumentNullException(nameof(options));

        logger = loggerFactory.CreateLogger(nameof(LanguageActionFilter));
        localizationOptions = options;
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        string culture = context.RouteData.Values["culture"]?.ToString();

        if (!string.IsNullOrWhiteSpace(culture))
        {
            logger.LogInformation($"Setting the culture from the URL: {culture}");

#if DNX46
            System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo(culture);
            System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture);
#else
            CultureInfo.CurrentCulture = new CultureInfo(culture);
            CultureInfo.CurrentUICulture = new CultureInfo(culture);
#endif
        }

        base.OnActionExecuting(context);
    }
}

On the controller I use the LanguageActionFilter.

[ServiceFilter(typeof(LanguageActionFilter))]
[Route("api/{culture}/[controller]")]
public class ProductsController : Controller
{
    ...
}

This works so far, but I have two issues with it:

  1. I don't like having to declare {culture} on every controller, as I am going to need it on every route.
  2. I having a default culture doesn't work with this approach, even if I declare it as [Route("api/{culture=en-US}/[controller]")] for obvious reasons.

Setting a default route results is not working neither.

app.UseMvc( routes =>
{
    routes.MapRoute(
        name: "DefaultRoute",
        template: "api/{culture=en-US}/{controller}"
    );
});

I also investigated in a custom IRequestCultureProvider implementation and add it to the UseRequestLocalization method like

app.UseRequestLocalization(new RequestLocalizationOptions
{
    RequestCultureProviders = new List<IRequestCultureProvider>
    {
        new UrlCultureProvider()
    },
    SupportedCultures = new List<CultureInfo>
    {
        new CultureInfo("de-de"),
        new CultureInfo("en-us"),
        new CultureInfo("en-gb")
    },
    SupportedUICultures = new List<CultureInfo>
    {
        new CultureInfo("de-de"),
        new CultureInfo("en-us"),
        new CultureInfo("en-gb")
    }
}, new RequestCulture("en-US"));

but then I don't have access to the routes there (I assume cause the routes are done later in the pipeline). Of course I could also try to parse the requested url. And I don't even know if I could change the route at this place so it would match the above route with the culture in it.

Passing the culture via query parameter or changing the order of the parameters inside the route is not an option.

Both urls api/en-us/products as we as api/products should route to the same controller, where the former don't change the culture.

The order in which the culture will be determined should be

  1. If defined in url, take it
  2. If not defined in url, check query string and use that
  3. If not defined in query, check cookies
  4. If not defined in cookie, use Accept-Language header.

2-4 is done via UseRequestLocalization and that works. Also I don't like the current approach having to add two attributes to each controller ({culture} in route and the [ServiceFilter(typeof(LanguageActionFilter))]).

Edit: I also like to limit the number of valid locales to the one set in SupportedCultures property of the RequestLocalizationOptions passed to the UseRequestLocalization.

IOptions<RequestLocalizationOptions> localizationOptions in the LanguageActionFilter above doesn't work as it returns a new instance of RequestLocalizationOptions where SupportedCultures is always null and not the one passed to the.

FWIW it's an RESTful WebApi project.

1 Answers
Related