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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
decimal number = 1.2m; | |
EvaluateJavaScript($"myFunction({number})"); |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//… | |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"myFunction(1.2)" |
But running on a PC with cs-CZ (Czech) culture we suddenly get:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"myFunction(1,2)" |
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
):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"myFunction(1.2)" |
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
:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using static System.FormattableString; | |
// now you can use just | |
EvaluateJavaScript(Invariant($"myFunction({number})")); |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var result = $"Some interpolated {number}".ToString(usCulture); |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Returns this string. | |
public override string ToString() | |
{ | |
return this; | |
} | |
// Returns this string. | |
public string ToString(IFormatProvider? provider) | |
{ | |
return this; | |
} |
So we need to do a small side-step instead:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
FormattableString formattable = $"Some interpolated {number}"; | |
Console.WriteLine(formattable.ToString(usCulture)); |
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:

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
.
Nice blog post, and immediately usable for me. Thanks for the tip!
Great article to get more insights on String interpolation, if anyone stumbles upon this and want to read basics of String interpolation check
Basics of String Interpolation in C#
thanks
Thanks! Now I can use string interpolation, for writing some lon/lat coordinates:
Invariant($"{location.Longitude} {location.Latitude}")
Which looks more ‘clean’ to me, than old-school string concatenation
location.Longitude.ToString(CultureInfo.InvariantCulture) + " " + location.Latitude.ToString(CultureInfo.InvariantCulture);
(where the InvariantCulture needs to be specified separately for each number).
Without
Invariant
, the numbers are indeed converted to strings using a,
comma separator instead of a.
dot. Because of nl-NL (Dutch/Netherlands) culture settings on my machine.