Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#6.0

SmartFormat.NET /2 - Enhancing string.Format to new levels

5.00/5 (5 votes)
3 Dec 2016MIT4 min read 20K  
SmartFormat.NET /2 is a string template library that allows you to fill a string with data. Easy to use, fast, extensible, and extremely powerful. Allows for named {placeholders} using any data type, conditional formatting, iterating through IEnumerables, and much more.

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:

C#
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:

C#
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

C#
var addrList = new[] {  new { Name = "Jim", Address = new {City = "New York", State = "NY"} } };
Smart.Format("{Name} from {Address.City}, {Address.State}", addrList);
// Outputs: "Jim from New York, NY"

Pluralization

C#
var emails = new List<string>() {"email1", "email2", "email3"};
Smart.Format("You have {0} new {0:message|messages}", emails.Count);
// Outputs: "You have 3 messages"

Gender Conjugation

C#
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

C#
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}});
// Outputs: "Jim, Pam, Dwight and Michael liked your comment"

Choose

C#
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}});
// Outputs: "You have 3 new messages"

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 catching 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:

C#
// missing or mal-formatted placeholders
var badPlaceholders = new HashSet<string>();
Smart.Default.OnFormattingFailure += (sender, args) => { badPlaceholders.Add(args.Placeholder); };

// parsing errors, like missing closing curly braces
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:

C#
string.Format("{{literal}}")
Smart.Format("{{literal}}")

This string.Format compatibility, however, causes problems when using SmartFormat's extended formatting capabilities, like

C#
// This won't work with default settings!
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:

// This will work
Smart.Default.Parser.UseAlternativeEscapeChar('\\');
Smart.Format("{0:{Persons:{Name}|, }}", model);

With this parser setting the output {literal} can be achieved by Smart.Format("\{literal\}").

Several Data Sources

Like with string.Format it is possible to use several data sources as parameters to SmartFormat. The concept however is a bit different:

C#
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);
// Outputs: "First dictionary: John, second dictionary: Washington"

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

 

License

This article, along with any associated source code and files, is licensed under The MIT License