Is this a good approach for temporarily changing the current thread's culture?

I work on a fairly large ASP .NET Web Forms application that is currently used primarily in the United States. We are in the process of rolling it out to other parts of the world, which of course means we are currently working on localizing all areas of the application. Generally speaking our approach has been to set the current thread's CurrentCulture and CurrentUICulture properties at the beginning of each request to support the proper formatting and resource extraction based on the current user's locale.

In some cases, however, we have a need to run a certain bit of a code using a culture other than the culture of the current user. For example, 'User A' lives in Germany but works for a company that does business with other companies in France. When 'User A' wants to create an invoice (PDF) for one of those French companies, we want that invoice generation code to run with the 'fr-FR' culture rather than the 'de-DE' culture.

I've considered a couple ways of doing this easily and am wondering if I'm going about this correctly. My main concerns are around performance and thread safety.

One approach involves a static method designed to run a given task with a provided culture. Something like this:

 public static void RunWithCulture(CultureInfo culture, Action task)
    {
        if (culture == null)
            throw new ArgumentNullException("culture");

        var originalCulture = new
                                  {
                                      Culture = Thread.CurrentThread.CurrentCulture,
                                      UICulture = Thread.CurrentThread.CurrentUICulture
                                  };

        try
        {
            Thread.CurrentThread.CurrentCulture = culture;
            Thread.CurrentThread.CurrentUICulture = culture;
            task();
        }
        finally
        {
            Thread.CurrentThread.CurrentCulture = originalCulture.Culture;
            Thread.CurrentThread.CurrentUICulture = originalCulture.UICulture;
        }
    }

This method could then be invoked like this:

var customerCulture = new CultureInfo(currentCustomer.Locale);
CultureRunner.RunWithCulture(customerCulture, () => invoiceService.CreateInvoice(currentCustomer.CustomerId));

I've also considered creating a class that implements IDisposable that would be responsible for setting the thread culture in it's ctor and then returning the original cultures back in the Dispose method, so you could call it like this:

var customerCulture = new CultureInfo(currentCustomer.Locale);
using(new CultureRunner(currentCustomer.Locale))
{
  invoiceService.CreateInvoice(currentCustomer.CustomerId);
}

Am I going about this all wrong? Which, if any of these approaches is preferable?

Answers


I like the using approach. I'd also create an extension method to make things read better:

var customerCulture = new CultureInfo(currentCustomer.Locale);  
using (customerCulture.AsCurrent()) {
  invoiceService.CreateInvoice(currentCustomer.CustomerId);
}

Something like this:

public static class CultureInfoExtensions {
  public static IDisposable AsCurrent(this CultureInfo culture) {
    return new CultureRunner(culture);
  }
}

CultureRunner example:

public class CultureRunner : IDisposable
{
    readonly CultureInfo originalCulture;
    readonly CultureInfo originalUICulture;

    public CultureRunner(CultureInfo culture)
    {
        if (culture == null)
            throw new ArgumentNullException(nameof(culture));

        originalCulture = Thread.CurrentThread.CurrentCulture;
        originalUICulture = Thread.CurrentThread.CurrentUICulture;

        Thread.CurrentThread.CurrentCulture = culture;
        Thread.CurrentThread.CurrentUICulture = culture;
    }

    public void Dispose()
    {
        Thread.CurrentThread.CurrentCulture = originalCulture;
        Thread.CurrentThread.CurrentUICulture = originalUICulture;
    }
}

Or, if it's always your customer object who sets the culture, another extension method would raise the abstraction even further:

using (currentCustomer.CultureContext()) {
  invoiceService.CreateInvoice(currentCustomer.CustomerId);
}

Since you are asking if temporary changing Current Thread's Culture is good idea, I can only answer: no. It could be used if and only there is no other way to get things working. That is just because such switching is error prone. OK, you won't forget to change things back with the code Jordão (respect) gave you, but... For now you have customers that want to create French invoices. I assuming that you want to use French date, number and currency formats. That's OK. But... What if in the future certain future would need to be printed out with other format, for example this originating German? Are you going to create some kind of ugly work-around?

I understand that it could be beyond your control (like Reporting Software could be 3rd party stand-alone solution and you could not control how it is handling ToString()) but if it is within your control, I would recommend feeding the data in right format in the first place. For example you could create some data transforming layer (DTO) and format data correctly (via ToString(IFormatProvider)). I know this is quite an effort but since you are asking about correct way to do things...

If we were in the same organization and I would do I18n code review, you could be sure that I would point out temporary changing culture as a defect. Usually there is a way to avoid this.


Need Your Help

C# 4.0, optional parameters and params do not work together

c# .net c#-4.0 params optional-parameters

How can i create a method that has optional parameters and params together?

ASP.NET WebApi unit testing with Request.CreateResponse

c# unit-testing asp.net-web-api mocking httpresponse

I am trying to write some unit tests for my ApiController and faced some issues. There is a nice extension method called Request.CreateResponse that helps a lot with generating response.