FormattableString as Method Parameter

September 21st 2018 C# EF Core

Interpolated strings in C# 6 introduced a simplified syntax for most use cases of String.Format. So, instead of writing:

var name = "World";
var formatted = string.Format("Hello, {0}!", name);

We can now simply write:

var name = "World";
var formatted = $"Hello, {name}!";

The new syntax has several immediate advantages, but its internal implementation also lends itself to some innovative usage. In EF Core 2.0, interpolated strings can be used to write safe SQL queries with shorter syntax:

using (var context = new EFContext())
{
    var p = "Doe";
    var query = context.Persons.FromSql($"SELECT * FROM Persons WHERE LastName = {p};");
    // ...
}

If you're not familiar with that feature, you would expect the above code to generate an invalid SQL query, susceptible to SQL injection. However, it will generate a perfectly valid and safe query:

SELECT * FROM Persons WHERE LastName = N'Doe';

How is that possible? Here's the signature of the method that gets called:

IQueryable<TEntity> FromSql<TEntity>(this IQueryable<TEntity> source,
                                     FormattableString sql) where TEntity : class;

Yes, interpolated strings are represented as FormattableString instances which do the formatting when they are converted to a string. The FromSql method intercepts the FormattableString instance passed to it and converts the arguments to proper SQL parameters.

Of course, you can write a method accepting a FormattableString, as well:

public static string SampleMethod(FormattableString formattable)
{
    var arguments = formattable.GetArguments()
                               .Select(arg => $"'{arg.ToString()}'").ToArray();
    return string.Format(formattable.Format, arguments);
}

My method intercepts the interpolated string before the formatting and puts all the arguments in quotes:

var name = "World";
var str = SampleMethod($"Hello, {name}!");

Assert.AreEqual("Hello, 'World'!", str);

I might also want to allow regular strings as method parameters. Since a string can't be converted to a FormattableString, I need to add another overload:

public static string SampleMethod(string str)
{
    return str;
}

Unfortunately, this will change the behavior of the method call with the interpolated string argument. It will now invoke the new string overload instead of the FormattableString one because the compiler prefers the conversion of interpolated strings to string over the conversion to FormattableString.

Of course, you could always force the FormattableString overload to be called if you use an explicit cast:

var str = SampleMethod((FormattableString)$"Hello, {name}!");

However, this makes the overload inconvenient to use. Let's take a look at how EF Core 2.0 managed to solve this issue. First, instead of using string directly for parameter type, we need to introduce a different type with an implicit conversion from string:

public class RawString
{
    public string Value { get; }

    private RawString(string str)
    {
        Value = str;
    }

    public static implicit operator RawString(string str)
    {
        return new RawString(str);
    }
}

We can now have the following two method overloads:

public static string SampleMethod(FormattableString formattable)
{
    var arguments = formattable.GetArguments()
                               .Select(arg => $"'{arg.ToString()}'").ToArray();
    return string.Format(formattable.Format, arguments);
}

public static string SampleMethod(RawString str)
{
    return str.Value;
}

It's still not enough, though. The method call with the interpolated string argument will now fail to compile with the following error:

The call is ambiguous between the following methods or properties: 'SampleClass.SampleMethod(FormattableString)' and 'SampleClass.SampleMethod(RawString)'

To resolve it, we need to add another implicit conversion to the RawString type, this time from the FormattableString type:

public static implicit operator RawString(FormattableString formattable)
{
    return new RawString(formattable.ToString());
}

This makes both calls (the one with the regular string argument and the one with the interpolated string argument) resolve correctly (to the RawString and to the FormattableString overload, respectively). It's exactly the approach that EF Core 2.0 uses.

My sample code is not really useful, but EF Core showed that interpolated strings can be used creatively to improve the API of a library. It's the only example I know of currently. But who knows, we might see similar solutions elsewhere in the future.

Copyright
Creative Commons License