InterpolatedStringBuilder is a FormattableString
with support for concatenating other interpolated strings, replace(), insert(), etc
It's similar to a StringBuilder but for Interpolated Strings (FormattableString)
- Install the NuGet package InterpolatedStrings or NuGet package InterpolatedStrings.StrongName
- Add
using InterpolatedStrings;
to your usings. - See examples below.
How to create an InterpolatedStringBuilder, append some more interpolated strings
string arg1 = "FormattableString";
var s = new InterpolatedStringBuilder($"This is exactly like {arg1}");
// s.Format now is equal to "This is exactly like {0}"
// s.Arguments contain [arg1]
string arg2 = "additional";
// += is an operator overload, but you can also call s.Append(...);
s += $"... but you can append {arg2} FormattableString instances";
// s.Format now is equal to "This is exactly like {0}... but you can append {1} FormattableString instances"
// s.Arguments now contains [arg1, arg2]
int categoryId = 1;
double maxPrice = 20.50;
//------------------------------------------------------------------------------
// Creates an initial SQL query, and appends more conditions.
// Embedded objects are NOT converted to strings: they are still kept
// as objects (in Arguments list), and the underlying format string just keeps
// the numbered placeholders
//------------------------------------------------------------------------------
var query = new InterpolatedStringBuilder($"SELECT * FROM Products");
query += $" WHERE CategoryId={categoryId}";
query += $" AND price<={maxPrice}";
// query.Format now is "SELECT * FROM Products WHERE CategoryId={0} AND price<={1}"
// query.Arguments now is [categoryId, maxPrice]
//------------------------------------------------------------------------------
// Then you can create your own methods (or extensions) to convert back from
// InterpolatedStringBuilder into a valid SQL statement
//------------------------------------------------------------------------------
string sql = string.Format(query.Format, query.Arguments.Select((arg, i) => "@p" + i.ToString()).ToArray());
Assert.AreEqual("SELECT * FROM Products WHERE CategoryId=@p0 AND price<=@p1", sql);
// If you were using Dapper you could pass parameters like this:
// var sqlParms = new DynamicParameters();
// for (int i = 0; i < query.Arguments.Count; i++) { dbArgs.Add("p" + i.ToString(), query.Arguments[i].Argument); }
// var products = connection.Query<Product>(sql, sqlParms)
int? categoryId = null;
double? maxPrice = 20.50;
//------------------------------------------------------------------------------
// Fluent API allows short syntax for appending multiple blocks,
// and using conditions
//------------------------------------------------------------------------------
var query =
new InterpolatedStringBuilder($"SELECT * FROM Products WHERE 1=1")
.AppendIf(categoryId != null, $" AND CategoryId={categoryId}")
.AppendIf(maxPrice != null, $" AND price<={maxPrice}");
// Now query.Format is "SELECT * FROM Products WHERE 1=1 AND price<={0}"
Using Raw String Literals:
int? categoryId = 3;
double? maxPrice = null;
//------------------------------------------------------------------------------
// Raw String Literals allows us to easily write multiline blocks
//------------------------------------------------------------------------------
var query = new InterpolatedStringBuilder($$"""
SELECT * FROM Products
/***where***/
ORDER BY Category, Name
""");
var wheres = new InterpolatedStringBuilder();
wheres.AppendIf(categoryId != null, $" AND CategoryId={categoryId}");
wheres.AppendIf(maxPrice != null, $" AND price<={maxPrice}");
if (wheres.Format.Length> 0)
{
wheres.Remove(0, " AND ".Length).Insert(0, $"WHERE ");
query.Replace("/***where***/", wheres);
}
Assert.AreEqual("""
SELECT * FROM Products
WHERE CategoryId={0}
ORDER BY Category, Name
""",
query.Format);
FormattableStrings are parsed using regex. If you're using .net6.0+ you can use the methods that use an InterpolatedStringHandler.
// InterpolatedStringFactory.Create() will use regex, while Create6() will use InterpolatedStringHandler
var builder = InterpolatedStringFactory.Default.Create6($"Hello {world}");
builder.Append6($" something...");
builder.AppendIf6(true, $" something else...");
One of the nice things of this library is that you can extend the InterpolatedStringBuilder
class and override methods like AppendLiteral()
, AppendArgument()
, or AddArgument()
, and manipulate the way that Format
is built or the way that Arguments
are created.
And you can do things like this.
See more examples in unit tests.
Whenever you write an interpolated string, the compiler can either convert it to a plain string or (if you specify the right type) it can keep the interpolated string as a FormattableString
.
The nice part of FormattableString (as compared to a plain string) is that it keeps the Arguments
(the objects that you interpolate) and the Format
(the literals around the arguments) isolated from each other.
And this allows a lot of clever usages like this.
The major limitation of FormattableString is that it's immutable: you can't append new interpolated strings, or modify it (Replace()/Insert()/Remove()).
PS: Actually FormattableString
is an abstract class (which we also implement). The limitation in case is from ConcreteFormattableString
which is the concrete type that the compiler uses when it creates an interpolated string.
No, it's NOT.
It's more like a replacement for ConcreteFormattableString
, but it's similar to a StringBuilder only in the sense that it's mutable (we can concatenate new interpolated strings), which is not possible in ConcreteFormattableString.
So in other words, InterpolatedStringBuilder
is a FormattableString
implementation that allows us to concatenate other interpolated strings, and offers some methods similar to methods that you would also have in a StringBuilder (Replace()
, Insert()
, Remove()
) etc).
So our methods are named like StringBuilder methods, but instead of operating on plain strings (like a StringBuilder), it wraps both Arguments
and Literals (Format
) - like a FormattableString
would do.
Having a single wrapper (which wraps both Arguments
and Format
, and lets them "walk side-by-side" - always in synch) makes things easier.
In a single statement you can both append one or more literals and one or more arguments.
// Using a StringBuilder we have to keep Arguments and Literals individually
var sql = new StringBuilder();
var dynamicParams = new DynamicParameters();
sql.Append("SELECT * FROM Product WHERE 1=1");
sql.Append(" AND Name LIKE @p0");
dynamicParams.Add("p0", productName);
sql.Append(" AND ProductSubcategoryID = @p1");
dynamicParams.Add("p1", subCategoryId);
// Using InterpolatedStringBuilder the Arguments and Literals walk side-by-side
var sql = new InterpolatedStringBuilder();
sql.Append($"SELECT * FROM Product WHERE 1=1");
sql.Append($" AND Name LIKE {productName}");
sql.Append($" AND ProductSubcategoryID = {subCategoryId}");
And by inheriting from InterpolatedStringBuilder
we can even hack the way that literals and arguments are processed (e.g. automatically add spaces, or even parse hints like sql.Append($" AND Name LIKE {productName:nvarchar(200)}")
).
Starting with net6.0 the interpolated strings can be parsed using an InterpolatedStringHandler
, which processes the interpolated strings block by block (literal by literal, argument by argument).
This step-by-step processing is very interesting because derived classes have a chance to modify the underlying format - like automatically adding spaces, adding or removing quotes, extracting IFormattable formats, or anything else.
Before InterpolatedStringHandler
the only way to do that (process each literal one by one) was using regular expressions.
Our StringInterpolationBuilder
works both with net5.0 or older (using regex) and with net6.0+ (using InterpolatedStringHandler
). You can inherit StringInterpolationBuilder
and override AppendLiteral()
and AppendArgument()
, and do your own magic.
If you don't need to override those methods then probably we wouldn't need to parse the format using regex (we'll improve that).
MIT License