What's a Good Way of Doing String Templating in .Net

What's a good way of doing string templating in .NET?

Use a templating engine. StringTemplate is one of those, and there are many.

Example:

using Antlr.StringTemplate;
using Antlr.StringTemplate.Language;

StringTemplate hello = new StringTemplate("Hello, $name$", typeof(DefaultTemplateLexer));
hello.SetAttribute("name", "World");
Console.Out.WriteLine(hello.ToString());

Dynamic String Templating in C#

You could pass arguments as dynamic object, iterate through its keys and utilize Regex to perform replacement:

private string ReplaceNameTags(string uri, dynamic arguments)
{
string result = uri;
var properties = arguments.GetType().GetProperties();
foreach (var prop in properties)
{
object propValue = prop.GetValue(arguments, null);
//result = Regex.Replace(uri, $"{{{prop.Name}}}", propValue.ToString());
result = Regex.Replace(result, $"{{{prop.Name}}}", propValue.ToString());
}
return result;
}

Example use would be:

ReplaceNameTags(uri, new { projectId, uploadId });

What can I use to get name string templating that goes beyond String.Format?

You could try RazorEngine, based on Razor.

  string template = 
@"<html>
<head>
<title>Hello @Model.Name</title>
</head>
<body>
Email: @Html.TextBoxFor(m => m.Email)
</body>
</html>";

var model = new PageModel { Name = "World", Email = "someone@somewhere.com" };
string result = Razor.Parse(template, model);

String Formatting in a .NET Templating Engine (ideally Nustache)

I've actually implemented my suggestion as it didn't take long at all to do. Seems to work very nicely. It's limited as it stands, as it presumes that {{x|y}} is with x as a decimal type and y as a number of decimal characters. I also used <### and ###> instead because $ is used in the regex replacement string (eg. $1 is the first matched group, etc). So using # meant I didn't have to escape them.

Just incase anyone's interested, here's the code:

using System.Text.RegularExpressions;

public static class NustacheUtil
{
public static string PreProcess(string templateString)
{
return Regex.Replace(templateString, @"{{(.*?)\|(\d+)}}", @"<###{{$1}}|$2###>");
}

public static string PostProcess(string templateString)
{
var result = templateString;

for(;;)
{
var match = Regex.Match(result, @"<###([0-9.]+?)\|(\d+)###>");

if(!match.Success || match.Groups.Count != 3)
{
break;
}

decimal value;

if(decimal.TryParse(match.Groups[1].Value, out value))
{
var numDecimalPlaces = match.Groups[2].Value;
result = result.Remove(match.Index, match.Length);
result = result.Insert(match.Index, value.ToString("N" + numDecimalPlaces));
}
}

return result;
}
}

And a few tests:

using NUnit.Framework;

[TestFixture]
class NustacheUtilTests
{
[Test]
public void PreProcessTest_Single()
{
// Arrange
const string templateString = "Test number: {{MyNumber|3}}";

// Act
var result = NustacheUtil.PreProcess(templateString);

// Assert
Assert.That(result, Is.EqualTo("Test number: <###{{MyNumber}}|3###>"));
}

[Test]
public void PostProcessTest_Single()
{
// Arrange
const string templateString = "Test number: <###123.456789|3###>";

// Act
var result = NustacheUtil.PostProcess(templateString);

// Assert
Assert.That(result, Is.EqualTo("Test number: 123.457"));
}

[Test]
public void PreProcessTest_Multiple()
{
// Arrange
const string templateString = "Test number: {{MyNumber|3}}, and another: {{OtherNumber|2}}";

// Act
var result = NustacheUtil.PreProcess(templateString);

// Assert
Assert.That(result, Is.EqualTo("Test number: <###{{MyNumber}}|3###>, and another: <###{{OtherNumber}}|2###>"));
}

[Test]
public void PostProcessTest_Multiple()
{
// Arrange
const string templateString = "Test number: <###123.457|3###>, and another: <###947.74933|2###>";

// Act
var result = NustacheUtil.PostProcess(templateString);

// Assert
Assert.That(result, Is.EqualTo("Test number: 123.457, and another: 947.75"));
}
}

And usage with Nustache:

var templateString = NustacheUtil.PreProcess(templateString);
templateString = Render.StringToString(templateString, dataSource);
templateString = NustacheUtil.PostProcess(templateString);

It obviously hasn't yet been used in anger. I'll update the code here when I make changes, as I'm sure I will make improvements as I use it properly.

fastest way to replace string in a template

From Atwood: It. Just. Doesn't. Matter.

Format string with meaningful markers

Please check out SmartFormat which in effect will do what you want to.

(You sayed, that you do not use replacing the human readable names with the indexes)

Example from their wiki:

String.Format references all args by index:

String.Format("{0} {1}", person.FirstName, person.LastName)

Smart.Format takes this a step further, and lets you use named placeholders instead:

Smart.Format("{FirstName} {LastName}", person)

In fact, Smart.Format supports several kinds of expressions:

Smart.Format("{FirstName.ToUpper} {LastName.ToLower}", person)

Working example requires the variables to be packed into an anonymous type:

var formatted=Smart.Format(template, new { orderNo, warehouseName, eta })

that returns correct, desired, result, with markers correctly replaced. Without the anonymous type it didn't seem to work.

When is it better to use String.Format vs string concatenation?

Before C# 6

To be honest, I think the first version is simpler - although I'd simplify it to:

xlsSheet.Write("C" + rowIndex, null, title);

I suspect other answers may talk about the performance hit, but to be honest it'll be minimal if present at all - and this concatenation version doesn't need to parse the format string.

Format strings are great for purposes of localisation etc, but in a case like this concatenation is simpler and works just as well.

With C# 6

String interpolation makes a lot of things simpler to read in C# 6. In this case, your second code becomes:

xlsSheet.Write($"C{rowIndex}", null, title);

which is probably the best option, IMO.

String Interpolation vs String.Format

The answer is both yes and no. ReSharper is fooling you by not showing a third variant, which is also the most performant. The two listed variants produce equal IL code, but the following will indeed give a boost:

myString += $"{x.ToString("x2")}";

Full test code

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Diagnostics.Windows;
using BenchmarkDotNet.Running;

namespace StringFormatPerformanceTest
{
[Config(typeof(Config))]
public class StringTests
{
private class Config : ManualConfig
{
public Config() => AddDiagnoser(MemoryDiagnoser.Default, new EtwProfiler());
}

[Params(42, 1337)]
public int Data;

[Benchmark] public string Format() => string.Format("{0:x2}", Data);
[Benchmark] public string Interpolate() => $"{Data:x2}";
[Benchmark] public string InterpolateExplicit() => $"{Data.ToString("x2")}";
}

class Program
{
static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<StringTests>();
}
}
}

Test results

|              Method | Data |      Mean |  Gen 0 | Allocated |
|-------------------- |----- |----------:|-------:|----------:|
| Format | 42 | 118.03 ns | 0.0178 | 56 B |
| Interpolate | 42 | 118.36 ns | 0.0178 | 56 B |
| InterpolateExplicit | 42 | 37.01 ns | 0.0102 | 32 B |
| Format | 1337 | 117.46 ns | 0.0176 | 56 B |
| Interpolate | 1337 | 113.86 ns | 0.0178 | 56 B |
| InterpolateExplicit | 1337 | 38.73 ns | 0.0102 | 32 B |

New test results (.NET 6)

Re-ran the test on .NET 6.0.9.41905, X64 RyuJIT AVX2.

|              Method | Data |      Mean |   Gen0 | Allocated |
|-------------------- |----- |----------:|-------:|----------:|
| Format | 42 | 37.47 ns | 0.0089 | 56 B |
| Interpolate | 42 | 57.61 ns | 0.0050 | 32 B |
| InterpolateExplicit | 42 | 11.46 ns | 0.0051 | 32 B |
| Format | 1337 | 39.49 ns | 0.0089 | 56 B |
| Interpolate | 1337 | 59.98 ns | 0.0050 | 32 B |
| InterpolateExplicit | 1337 | 12.85 ns | 0.0051 | 32 B |

The InterpolateExplicit() method is faster since we now explicitly tell the compiler to use a string. No need to box the object to be formatted. Boxing is indeed very costly. Also note that NET 6 reduced both CPU and memory allocations - for all methods.

New test results (.NET 7)

Re-ran the test on .NET 7.0.122.56804, X64 RyuJIT AVX2.

|              Method | Data |      Mean |   Gen0 | Allocated |
|-------------------- |----- |----------:|-------:|----------:|
| Format | 42 | 41.04 ns | 0.0089 | 56 B |
| Interpolate | 42 | 65.82 ns | 0.0050 | 32 B |
| InterpolateExplicit | 42 | 12.19 ns | 0.0051 | 32 B |
| Format | 1337 | 41.02 ns | 0.0089 | 56 B |
| Interpolate | 1337 | 59.61 ns | 0.0050 | 32 B |
| InterpolateExplicit | 1337 | 13.28 ns | 0.0051 | 32 B |

No significant changes since .NET 6.



Related Topics



Leave a reply



Submit