12

Background

I'm creating a simple, multilingual website using ASP.NET 4.6, C#, OWIN pipeline on IIS (Microsoft.Owin.Host.SystemWeb), lots of asynchronous method calls and standard, global resource files (*.resx in App_GlobalResources). The website uses MVC5, WebAPI2 and Autofac as dependency resolver.

Problem

I can't correctly change the locale/culture of generated pages, because asynchronous methods use multiple threads per request and I can't find a way to set Thread.Current[UI]Culture for every thread associated with given request as those properties aren't synchronized. I would also like to stay with clean code without "async/await culture configuration" messing with useful code.

Code

Startup.cs

public void Configuration(IAppBuilder app)
{
    ...

    app.UseAutofacMiddleware(container);
    app.UseAutofacMvc();
    app.UseAutofacWebApi(httpConfiguration);
    app.UseWebApi(httpConfiguration);

    ...

    app.Use(async (ctx, next) =>
    {
        /* in production, detect based on current URL and/or cookie */
        var culture = new CultureInfo("pl_PL");

        CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = culture;

        await next.Invoke();
    });
}

SampleController.cs

public async Task<ActionResult> SayHello()
{
    // returns pl_PL message
    var msgA = Resources.Messages.HelloWorld; 

    await someService.doSthAsync();

    // returns system default (en_US) message
    var msgB = Resources.Messages.HelloWorld;

    return Content(msgA + " - " + msgB);
}

  1. Should I create a custom [AspNet]SynchronizationContext as suggested in this SO answer? If that is the case how should I do that?
  2. Should I give up on global resouorces as source of translations and use some other approach? If so, what (library?) could I use?
Cœur
  • 37,241
  • 25
  • 195
  • 267
Crozin
  • 43,890
  • 13
  • 88
  • 135
  • You could save the culture at the start of the action method. Doesn't `SampleController` derive from [Controller](https://msdn.microsoft.com/library/system.web.mvc.controller "Controller Class (System.Web.Mvc)")? You can get to the current HTTP context and culture from there? – Paulo Morgado Aug 20 '15 at 23:40
  • I could detect the culture at the start of the action method but where should I assign it in order to preserve it across multiple threads used to serve a single HTTP response? – Crozin Aug 21 '15 at 05:50
  • Have you looked at the code on the link you posted? – Paulo Morgado Aug 21 '15 at 11:35
  • Yes, I did, but it requires additional "dirty" code which I'd like to avoid. It also suggests chaning implementation of synchronization context but I'm not quite sure how to do that correctly. – Crozin Aug 26 '15 at 07:13
  • the code is not for the implementation of the synchronization context. The code is like what the EntityFramework sore for ASP.NET Identity 2 uses. – Paulo Morgado Aug 26 '15 at 10:38

2 Answers2

5

What about using the Owing Globalization Library, it seems like it was created just for this purpose.

You can still use your resx to localize your resources, and it has great customization capabilities:

public void Configuration(IAppBuilder app)
{
    ...
    app.UseGlobalization(new OwinGlobalizationOptions("en-US",true)
       .DisablePaths("Content", "bundles")
       .Add("fr-FR", true).AddCustomSeeker(new CultureFromPreferences()));
}

I have tested the use of async/await and the culture is preserved:

    public async Task<ActionResult> About()
    {
        var msgA = Resources.Resources.About;

        await Task.Delay(1000);

        var msgB = Resources.Resources.About;

        ViewBag.Message = msgA + " - " + msgB;

        return View();
    }

Note: I'm not the author of the library, I happen to have used it before.

Julien Lebot
  • 3,092
  • 20
  • 32
1

The answer by Joe Enzmiger looks appropriate here:

public override Task<HttpResponseMessage> ExecuteAsync(HttpControllerContext     controllerContext, CancellationToken cancellationToken)
{
if (controllerContext.Request.Headers.AcceptLanguage != null && 
    controllerContext.Request.Headers.AcceptLanguage.Count > 0)
{
    string language = controllerContext.Request.Headers.AcceptLanguage.First().Value;
    var culture = CultureInfo.CreateSpecificCulture(language);
    HttpContext.Current.Items["Culture"] = culture;
    //Thread.CurrentThread.CurrentCulture = culture;
    //Thread.CurrentThread.CurrentUICulture = culture;
}

base.ExecuteAsync(controllerContext, cancellationToken); 
}

and then, in any task you need the culture:

var culture = HttpContext.Current != null ? HttpContext.Current.Items["Culture"] as CultureInfo : Thread.CurrentThread.CurrentCulture;
Community
  • 1
  • 1