Using proper culture with C# string interpolation

Once you get used to string interpolation feature in C# it is easy to get addicted to it. However, we need to keep in mind the fact that the current system culture matters. Consider we are building a code that communicates with JavaScript – for example in Web Assembly – and needs to execute a function that takes in a decimal number as an argument:

decimal number = 1.2m;
EvaluateJavaScript($"myFunction({number})");

view raw
InterpolationEval.cs
hosted with ❤ by GitHub

As simple as our sample looks, it contains a remarkable bug. Why?

String interpolation was built as an alternative to the string.Format method. In fact, in the generated IL we can see it actually evaluates to string.Format call:

//…
IL_001a: call string [System.Runtime]System.String::Format(string, object)
IL_001f: call instance void CultureStringInterpolation.Program::EvaluateJavaScript(string)

The output of string.Format depends on the CultureInfo.CurrentCulture and so does string interpolation’s output. Hence, when you run this code on a PC with the en-US (American English) culture, it evaluates to:

"myFunction(1.2)"

view raw
EvaluateUS.cs
hosted with ❤ by GitHub

But running on a PC with cs-CZ (Czech) culture we suddenly get:

"myFunction(1,2)"

view raw
EvaluateCS.cs
hosted with ❤ by GitHub

The decimal comma would certainly be appropriate when displayed to the user. In our case, however, this would mean a call to the myFunction function with two arguments – 1 and 2!

We could use string.Format directly to solve this problem, as it allows us to specify a IFormatProvider to control formatting. But can we keep using an interpolated string and evaluate it to a machine-friendly representation?

Solution

As I have mentioned, interpolated string in most cases evaluates to a plain old string.Format call. However, when needed, the syntax can evaluate to an instance of System.FormattableString. This helpful class essentially wraps the interpolated string and gives read-only access to the format string, its arguments, and it also offers a static Invariant method. This method takes a single FormattableString argument (your interpolated string) and evaluates it according to the Invariant culture (CultureInfo.InvariantCulture):

decimal number = 1.2m;
EvaluateJavaScript(FormattableString.Invariant($"myFunction({number})"));

That is ideal, as invariant culture always stays the same, so we can rest assured that the interpolated string evaluation will always result in:

"myFunction(1.2)"

view raw
EvaluateUS.cs
hosted with ❤ by GitHub

Tip: FormattableString.Invariant is a mouthful. If you use it often, you can utilize the using static feature, which was introduced in C# 6 and then reference the method directly as Invariant:

using static System.FormattableString;
// now you can use just
EvaluateJavaScript(Invariant($"myFunction({number})"));

view raw
UsingStatic.cs
hosted with ❤ by GitHub

Specifying culture

You might be wondering now if there is a method that would allow us to use a specific culture to use when evaluating an interpolated string. The first attempt might be to just do the following:

var result = $"Some interpolated {number}".ToString(usCulture);

view raw
ToStringBad.cs
hosted with ❤ by GitHub

Unfortunately, this does not work as we expect. Because we are not explicitly casting the interpolated string to FormattableString, it gets directly evaluated to string.Format, so we are then just calling ToString(usCulture) on the resulting string. And string.ToString implementations are in fact just no-ops:

// Returns this string.
public override string ToString()
{
return this;
}
// Returns this string.
public string ToString(IFormatProvider? provider)
{
return this;
}

view raw
string.ToString.cs
hosted with ❤ by GitHub

So we need to do a small side-step instead:

FormattableString formattable = $"Some interpolated {number}";
Console.WriteLine(formattable.ToString(usCulture));

view raw
ToStringGood.cs
hosted with ❤ by GitHub

By explicitly storing the interpolated string in a FormattableString variable, we can now call the FormattableString.ToString(IFormatProvider) implementation, which actually does what we need – evaluates the interpolation under the given culture.

Source code

Example source code for this blog post is available on my GitHub. It showcases all discussed options in a single .NET Core 3.0 console app:

Invariant culture sample
Invariant culture sample

Summary

Interpolated strings in C# are handy, but we have to ensure that they are evaluated correctly according to the context where the resulting string is presented. If we are displaying it to the user, the default behavior of using CultureInfo.CurrentCulture is probably most appropriate. On the other hand, in machine-to-machine communication, we should always make sure to use invariant culture using FormattableString.Invariant.

Buy me a coffeeBuy me a coffee

1 thought on “Using proper culture with C# string interpolation”

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.