Introduction
When it was time for an improved release of MailMergeLib - A .NET Mail Client Library I was looking for a fast string template library, which should substitute the low performing regular expressions that were used for replacing the placeholders in the mail text with variable values. I had a closer look at several solutions like this one or that one. But none of them came close to what Scott Rippey came up with some time ago: SmartFormat.NET. Having ten-thousands of downloads from NuGet this library is also quite popular.
Background
The criteria I had for the string template library were:
- Open source, preferably licensed under The MIT License
- True parsing, no
RegEx
- Compatible with
string.Format
- Small footprint in C# code, no dependency on external assemblies
- Extensibility
- Performance close to
string.Format
- Possible to port to .NET Core
- Easy to integrate into the Mail Client Library
These criteria also lead to rejecting Razor Engine.
Unfortunately Scott and his project contributors had different priorities for some time, and so the project was kind of dormant for a while. So I decided to contribute to the project, to take care of the issues that were reported by users, to port it to .Net Core (besides .NET Framework 4.0 and 4.5), and to extend it where I felt some deficiencies.
What about string interpolation coming with C# 6?
String interpolation is a helpful thing:
var addrList = new[] { new { Name = "Jim", Address = new { City = "New York", State = "NY" } } };
var result = string.Format($"{addrList[0].Name} in {addrList[0].Address.City}");
String interpolation, however, is only available at compile time. So I was looking for a similar solution, which is available at runtime. That's pretty much the target of SmartFormat.
What about Roslyn?
Yes, you can use CSharpScript
for compilation of an interpolated string like that:
namespace Roslyn
{
public class Program
{
public class Sample
{
public readonly int s = 12345;
}
static void Main(string[] args)
{
var sw = new Stopwatch();
sw.Start();
var state = CSharpScript.RunAsync(@"return string.Format($""The number is{s}."");", globals: new Sample()).Result;
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
}
}
}
The sample already includes a stop watch, which will make very clear, that this is not an alternative: It's a "dimension" away from string.Format
or SmartFormat performance.
Using the code
The syntax for using SmartFormat.NET /2 is still close to what Scott had described in his article here on Code Project, but the method has changed from "CustomFormat" to "Smart.Format". And SmartFormat.NET /2 has a very good Wiki as well. Here are just a few examples with the up-to-date syntax to make you curious for more:
Named Placeholders
var addrList = new[] { new { Name = "Jim", Address = new {City = "New York", State = "NY"} } };
Smart.Format("{Name} from {Address.City}, {Address.State}", addrList);
Pluralization
var emails = new List<string>() {"email1", "email2", "email3"};
Smart.Format("You have {0} new {0:message|messages}", emails.Count);
Gender Conjugation
var user = new[] { new { Name = "John", Gender = 0 }, new { Name = "Mary", Gender = 1 } };
Smart.Default.Parser.UseAlternativeEscapeChar('\\');
Smart.Format("{1:{Name}} commented on {1:{Gender:his|her|their}} photo", user);
Lists
var Users = new[] { new { Name = "Jim" }, new { Name = "Pam" }, new { Name = "Dwight" }, new { Name = "Michael" } };
Smart.Format("{Users:{Name}|, | and } liked your comment", new object[] { new {Users = Users}});
Choose
var emails = new List<string>() {"email1", "email2", "email3"};
Smart.Format("You have {Messages.Count:choose(0|1):no new messages|a new message|{} new messages}", new object[] {new {Messages = emails}});
Common Pitfalls
Some users of SmartFormat.NET came across some issues and pitfalls with are extremely helpful to know when getting started. I also added these hints to the SmartFormat.NET /2 Wiki.
Smart.Format vs. SmartFormatter.Format
When you start to use the library, us Smart.Format(...)
. Smart.Format() automatically initializes all you need behind the scenes. If you're using SmartFormatter.Format(...)
directly, then it's your job to initialize. So for the beginning just leave SmartFormatter.Format() alone. Note: Smart.Format(...)
is just the short version for Smart.Default.Format(...)
.
Error Handling
By default, SmartFormat sets ErrorAction
for formatter and parser to ErrorAction.Ignore
. This can lead to confusing results. It's highly recommended, to turn exceptions on while developing and debugging the code:
Smart.Default.ErrorAction = ErrorAction.ThrowError;
Smart.Default.Parser.ErrorAction = ErrorAction.ThrowError;
Error Tracking
Besides throwing and catch
ing exceptions it is possible, to trace any formatting or parsing errors by subscribing to corresponding events. When using Smart.Format(...)
these events can be processed like this:
var badPlaceholders = new HashSet<string>();
Smart.Default.OnFormattingFailure += (sender, args) => { badPlaceholders.Add(args.Placeholder); };
var parsingErrorText = new HashSet<string>();
Smart.Default.Parser.OnParsingFailure += (sender, args) => { parsingErrorText.Add(args.RawText); };
These events fire no matter how ErrorAction
of the formatter or parser are set. Opposed to exceptions, all errors will be reported, not only the first failure. Going this way you can decide in your code more fine grained, how to deal with errors.
Escaping Curly Braces
Out of the box SmartFormat is a drop-in replacement for string.Format
. The consequence is, that curly braces are escaped the string.Format
way. So if the desired output shall be {literal}
, it means doubling the open and closing curly braces:
string.Format("{{literal}}")
Smart.Format("{{literal}}")
This string.Format
compatibility, however, causes problems when using SmartFormat's extended formatting capabilities, like
Smart.Format("{0:{Persons:{Name}|, }}", model);
The reason is the double curly braces at the end of the format string, which the parser will escape in the string.Format
style, leading to a "missing closing brace" exception. Luckily, this is easy to solve:
Smart.Default.Parser.UseAlternativeEscapeChar('\\');
Smart.Format("{0:{Persons:{Name}|, }}", model);
With this parser setting the output {literal}
can be achieved by Smart.Format("\{literal\}")
.
Like with string.Format
it is possible to use several data sources as parameters to SmartFormat. The concept however is a bit different:
var dict1 = new Dictionary<string, string="">() { {"Name", "John"} };
var dict2 = new Dictionary<string, string="">() { { "City", "Washington" } };
var result = Smart.Format("First dictionary: {0:{Name}}, second dictionary: {1:{City}}", dict1, dict2);
Security
When running on .NET Framework 4.x, SmartFormat is using System.Runtime.Remoting.Messaging.CallContext.LogicalGet|SetData
in its ListFormatter
. With this in place, we know that dotnetfiddle.net will throw a security exception. When compiling SmartFormat for .Net Core, LogicalGet|SetData
is replaced by AsyncLocal<T>
, which does not have this issue. Unfortunately AsyncLocal<T>
is supported only on .NET Framework version 4.6 or later.
History
- 2016-12-04: Initial article for SmartFormat.NET /2