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

2

24

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?

Auster answered 26/4, 2011 at 15:57 Comment(1)
the using approach is the best one for me too.Twelvetone
H
18

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);
}
Hyperon answered 26/4, 2011 at 16:9 Comment(1)
You could add a more generic sample, since CultureRunner is not declared. I need to run a method using a different culture. Can you update your answer?Dagan
D
2

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.

Distrust answered 26/4, 2011 at 16:55 Comment(4)
Thanks for the response. When you say "... such switching is error prone" do you mean that it can lead to code that is difficult to maintain, or that this approach can lead to actual runtime errors in the application?Auster
Also, one of the reasons I did not opt to use the ToString(IFormatProvider) approach is I need more than just the correct date and number formats; I also need to pull in some static text in the correct languages from resx files. I have not yet found an effective and safe way to pull in a message with the current language short of changing the CurretUICulture property on the current thread.Auster
By error prone I meant maintaining. It should not cause errors in other parts of application, that is at least when you remember to switch it back.Introject
As for reading from resx files, you can always instantiate ResourceManager and use GetString() with appropriate CultureInfo.Introject

© 2022 - 2024 — McMap. All rights reserved.