C#: a dynamic string interpolation

Since C# 6, the string interpolation is with us, allowing for a more readable and understandable way of concatenating, formatting and manipulating strings. The older string.Format method (aka Composite formatting) relies on the exact position of the value in a string being formatted, so it is less intuitive and less comprehensible for a reader.

Compare:
var topicName = "formatting";

var compositeFormattingOutput = string.Format("You are reading an article on {0}", topicName);

var interpolationOutput = $"You are reading an article on {topicName}";

The {topicName} is referred to as interpolatedExpression, a placeholder or just as an Expression in the following text.

The readability of the code is paid for by performance as the string interpolation is slightly slower than the string.Format method. For more details on this topic, see this article.

Issue


Imagine a case when the interpolated template (string)  is just a string, stored in a resource file or in a database, and the values for the interpolation are not available during the code compilation.
At run-time, we would like to combine the template with an object to get a formatted output  - providing the object has properties named exactly like interpolatedExpressions in the template, e.g.:




How to achieve that?

Template conversion

First, the template with interpolatedExpression (Topic)  must be converted to a composite format, so from this:
You are reading an article on {Topic}

we have to get this:
You are reading an article on {0}

As you can notice, the {Topic} has been transformed to {0}.

At the same time, we have to keep a map between the position and the original name, e.g.
0 -> Topic

To achieve that, let's define an interface for this operation:

public interface IGetCompositeFormatStringDescriptionAction
{
    ICompositeFormatStringDescription Execute(string interpolatedFormatValue);
}

The Execute method takes the template ("You are reading an article on {Topic}" and returns the instance implementing ICompositeFormatStringDescription interface:

public interface ICompositeFormatStringDescription
{
    string CompositeFormatString { get; }

    IList<string> OrderedPlaceholderNames { get; }
}

where:
  •  the CompositeFormatString property contains the converted template ("You are reading an article on {0}" 
  •  the OrderedPlaceholderNames property contains a list of interpolatedExpressions from the original string in the order they occur in it (in this case { "Topic" } )

Input item conversion

The instance/object used during the formatting as a source of values for the template's expressions must be converted too. We need to get a list of all the property's names and values and convert this list to a Dictionary. The name of the property is used as a key, the value of the property as a value, e.g.

["Topic" :"string formatting options]

The interface is defined as follows:

public interface IInstanceToDictionaryConverter
{
    Dictionary Convert(TInstance instance);
}

Putting it all together

To format a template and get the output value, let's define the following interface:
public interface IInterpolatedStringFormatter

{
    string Format<TInstance>(string format, TInstance instance);

}

In the class implementing this interface, as the template is a composite format string and the instance should have the properties needed for the template, the well-known string.Format method is used to get the output.

Example

Note: the implementation of all the above-mentioned interfaces can be found on Github - see the link at the bottom of the article.

As you can see from the following code excerpt, the same string template ("Hello {Name}") can be used with instances of different classes. These classes, besides property's names, have nothing in common (no inheritance, no common interfaces).


var person = new Person { Name = "Martin" };
var net = new Thing { Name = "Internet", Owner = person };
var something = new Something { Name = "Doe" };

Console.WriteLine(formatter.Format("Hello {Name}", person));
Console.WriteLine(formatter.Format("Hello {Name}", something));
Console.WriteLine(formatter.Format("Hello {Name}", net));
Console.WriteLine(formatter.Format("Hello {Name}, owned by {Owner.Name}", net));

internal class Person
{
   public string Name { get; set; }
}

internal class Thing
{
   public string Name { get; set; }

   public Person Owner { get; set; }
}

internal class Something
{
   public T Name { get; set; }
}

Conclusion

This approach can be used when we would like to keep the template strings to be easily readable and not tightly coupled with a  particular class or an interface. 

The actual implementation of all the described interfaces together with a runnable example is available on GitHub


Links - other sources

It is possible to find some other sources on the same topic, for example: https://haacked.com/archive/2009/01/04/fun-with-named-formats-string-parsing-and-edge-cases.aspx/

No comments:

Post a Comment