Advanced String Templates in C#
Tags: csharp, dotnet
Published: 2019-12-20 20:01:30

c# 6 introduced string interpolation syntax. This feature allows for formatted text to be easily declared in code. For example:

```csharp
var name = "World";
var message = $"Hello, {name}"; // "Hello, World"
```

At it's most basic, this is syntactic sugar on top of c#'s `string.Format` method, but with a much cleaner syntax. However, simply interpolating strings is often a dangerous proposition. Consider the following:

```csharp
var url = $"https://api.example.com/sample?arg={arg}";
```

This will work in most cases, but if the parameter includes non-url-safe characters, this is likely to break. This could even expose a serious security vulnerability, expecially if you're doing string interpolation for HTML, javascript, or SQL (don't ever do this!).


## Formattable String

Hidden deep in the c# language spec is a minor note about string interpolation. In general, interpolated strings are compiled to a call to `string.Format`, but you can also cast it to a `FormattableString`. This type represents the string template and an array of objects which will be interpolated into it.

```csharp
var make = "Chrysler";
var model = "Town & Country";
var url = (FormattableString)$"https://api.example.com/vehicles?make={make}&model={model}";

Console.WriteLine(url.Format); // "https://api.example.com/vehicles?make={0}&model={1}";
Console.WriteLine(url.GetArgument(0)); // "Chrysler";
Console.WriteLine(url.GetArgument(1)); // "Town & Country";
```

This provides some interesting opportunities to create much more powerful string templating tools. For example, if we wanted to automatically encode the URL arguments, we can do the following:

```csharp
public static class Format
{
  public static Uri Uri(FormattableString template)
  {
    var encodedArgs = new object[template.ArgumentCount];

    for (var i = 0; i < template.ArgumentCount; i++)
    {
      var original = template.GetArgument(i);
      encodedArgs[i] = HttpUtility.UrlEncode(original);
    }

    return new Uri(string.Format(template.Format, encodedArgs));
  }
}
```

The above code creates a new array and populates it with the url-encoded arguments. It then calls `string.Format` with the original template and new encoded arguments and returns it as a Uri to indicate that it's been safely encoded.

to use it, we can call

```csharp
var make = "Chrysler";
var model = "Town & Country";
var url = Format.Uri($"https://api.example.com/vehicles?make={make}&model={model}");

Console.WriteLine(url); // https://api.example.com/vehicles?make=Chrysler&model=Town+%26+Country
```

## Custom Formats

Another interesting feature we can make (ab)use of is custom format strings. In a traditional c# string template, you can specify a format for each argument, I.E.

```csharp
Console.WriteLine($"Today is {DateTime.Now:yyyy-MM-dd}"); // Today is 2019-12-13
```

This is effectively the equivelant of calling `dateTime.ToString("yyyy-MM-dd")`. Any object that implements `IFormattable` can be used with a custom format string, which gives us an opportunity to define a simple syntax when working with string templates. In this example, we'll set up a simple HTML template that will either html encode a value or format it as markdown.

```csharp
public static HtmlString Html(FormattableString template)
{
  var encodedArgs = new object[template.ArgumentCount];

  for (var i = 0; i < template.ArgumentCount; i++)
  {
    encodedArgs[i] = new HtmlArgument(template.GetArgument(i));
  }

  return new HtmlString(string.Format(template.Format, encodedArgs));
}

class HtmlArgument : IFormattable
{
  public HtmlArgument(object value)
  {
    Value = value;
  }

  public object Value { get; }

  public string ToString(string format, IFormatProvider formatProvider)
  {
    switch (format)
    {
      case "markdown":
        return new Markdown().Transform(Value.ToString());
      case "dangerous-raw-html":
        return Value.ToString();
      default:
        return HttpUtility.HtmlEncode(Value);
    }
  }
}
```

We can then use this as follows:

```csharp
var html = Format.Html($"<article><h1>{title}</h1>{content:markdown}</article>");
```

`title` will be safely HTML encoded, and `content` will be rendered as markdown.

## Wrapping Up

String interpolation in c# is convenient, but can lead to some traps. If not used carefully, it can break with edge cases or even introduce vulnerabilities. Formattable strings are a little known, but potentially quite useful feature in c# that can be used to make string interpolation smarter and more context-aware.