We ran into something interesting the other day when one of our services crashed. The problem turned out to be an unhandled exception that was allowed to propagate up the call stack of a thread created by a third-party unmanaged DLL, but that wasn’t the interesting part. What got our attention was the source of the exception, in a line similar to this:
String.Format("blah blah {0:mm:ss:fff}", myTimeSpan);
We had just upgraded our entire platform to .NET 4.0, and suddenly a line of code that had been in production for over a year was failing. Neat! A little poking around isolated the problem, which is illustrated by the following:
class Program { static void Main(string[] args) { TimeSpan ts = new TimeSpan(12, 30, 30); try { Console.WriteLine(Environment.Version); Console.WriteLine(String.Format("{0:hh:mm:ss}", ts)); } catch (Exception ex) { Console.WriteLine(ex.Message); Console.WriteLine(ex.StackTrace); } Console.WriteLine("Press any key..."); Console.ReadKey(); } }
Compile that code for .NET 3.5 and it runs fine. Compile it for .NET 4.0 and it throws “Input string not in the correct format.” A hint as to why can be found in Microsoft’s MSDN page for the 4.0 version of TimeSpan:
Beginning with the .NET Framework version 4, the TimeSpan structure supports culture-sensitive formatting through the overloads of its ToString method, which converts a TimeSpan value to its string representation.
So in .NET 4 you can use overloads of TimeSpan.ToString() for culture-sensitive formatting of TimeSpan values, just as you already could with DateTime. It turns out that String.Format() now delegates to those overloads when it has to format a TimeSpan. All well and good, but why is it failing? A little further down on the same page is another clue:
In some cases, code that successfully formats TimeSpan values in .NET Framework 3.5 and earlier versions fails in .NET Framework 4. This is most common in code that calls a composite formatting method to format a TimeSpan value with a format string.
But our format string appeared to contain only valid format specifiers and delimeters. A little more digging turned up this page, describing custom TimeSpan format strings, which at the very end contained another clue:
Any other unescaped character in a format string, including a white-space character, is interpreted as a custom format specifier. In most cases, the presence of any other unescaped character results in a FormatException. There are two ways to include a literal character in a format string: enclose it in single quotation marks (the literal string delimiter); or precede it with a backslash (“\”), which is interpreted as an escape character. This means that, in C#, the format string must either be @-quoted, or the literal character must be preceded by an additional backslash.
The colon is a special character in a format string. It delimits the parameter array index value from the rest of the format, i.e. “{0:yadayada}”. If you put another colon in there, as we did to delimit minutes, seconds, and milliseconds, you must now escape it. So in .NET 4 the correct version of the test program would be:
class Program { static void Main(string[] args) { TimeSpan ts = new TimeSpan(12, 30, 30); try { Console.WriteLine(Environment.Version); Console.WriteLine(String.Format(@"{0:hh\:mm\:ss}", ts)); } catch (Exception ex) { Console.WriteLine(ex.Message); Console.WriteLine(ex.StackTrace); } Console.WriteLine("Press any key..."); Console.ReadKey(); } }
If you compile and run that against .NET 4 it works fine. Note the use of the @ to prevent the compiler from parsing escape characters in the format string. You could also use double backslashes.
If you have a lot of code that formats TimeSpans this could obviously be a pain. Fortunately our exposure was limited, and there is a workaround that I will describe below that will save you a ton of effort. But before that let’s take a look at DateTime, and see if it also follows the new behavior. I think we should be able to count on some consistency here. The test program modified to test a DateTime looks like this:
class Program { static void Main(string[] args) { DateTime dt = DateTime.Now; try { Console.WriteLine(Environment.Version); Console.WriteLine(String.Format("{0:hh:mm:ss}", dt)); } catch (Exception ex) { Console.WriteLine(ex.Message); Console.WriteLine(ex.StackTrace); } Console.WriteLine("Press any key..."); Console.ReadKey(); } }
But wait, when we run this it works fine. DateTime’s formatting methods still allow colons to appear in the format string after the first delimiter. That presents us with an important decision: if we do have a lot of code that formats TimeSpans, should we change it? Is Microsoft going to make this behavior consistent in the future? If so, which one wins? Personally, I wouldn’t change anything at this point, and the good news is that you don’t have to change any C# code to make this problem disappear.
Instead, you can add an element to the runTime section of your application configuration file, like this:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <runtime> <TimeSpan_LegacyFormatMode enabled="true" /> </runtime> </configuration>
I can confirm this works, and that it gives me indigestion to have to add even more crap to our already voluminous configuration files, but for some people it might make more sense than changing code, at least until Microsoft clarifies which way they are going with this.