FormattableString as Method Parameter
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.