The implementation I'd come up with very similar to giganoide's allowing callers to substitute the name used in the template for the data to interpolate from. This can accommodate things like feeding it anonymous types. It will used the provided interpolateDataAs name if provided, otherwise if it isn't an anonymous type it will default to the type name, otherwise it expects "data". (or specifically the name of the property) It's written as an injectable dependency but should still work as an extension method.
public interface ITemplateInterpolator
{
/// <summary>
/// Attempt to interpolate the provided template string using
/// the data provided.
/// </summary>
/// <remarks>
/// Templates may want to use a meaninful interpolation name
/// like "enquiry.FieldName" or "employee.FieldName" rather than
/// "data.FieldName". Use the interpolateDataAs to pass "enquiry"
/// for example to substitute the default "data" prefix.
/// </remarks>
string? Interpolate<TData>(string template, TData data, string? interpolateDataAs = null) where TData : class;
}
public class TemplateInterpolator : ITemplateInterpolator
{
/// <summary>
/// <see cref="ITemplateInterpolator.Interpolate(string, dynamic, string)"/>
/// </summary>
string? ITemplateInterpolator.Interpolate<TData>(string template, TData data, string? interpolateDataAs)
{
if (string.IsNullOrEmpty(template))
return template;
if (string.IsNullOrEmpty(interpolateDataAs))
{
interpolateDataAs = !typeof(TData).IsAnonymousType() ? typeof(TData).Name : nameof(data);
}
var parsed = Regex.Replace(template, @"{(?<exp>[^}]+)}", match =>
{
var param = Expression.Parameter(typeof(TData), interpolateDataAs);
var e = System.Linq.Dynamic.Core.DynamicExpressionParser.ParseLambda(new[] { param }, null, match.Groups["exp"].Value);
return (e.Compile().DynamicInvoke(data) ?? string.Empty).ToString();
});
return parsed;
}
For detecting the anonymous type:
public static bool IsAnonymousType(this Type type)
{
if (type.IsGenericType)
{
var definition = type.GetGenericTypeDefinition();
if (definition.IsClass && definition.IsSealed && definition.Attributes.HasFlag(TypeAttributes.NotPublic))
{
var attributes = definition.GetCustomAttributes(typeof(CompilerGeneratedAttribute), false);
return (attributes != null && attributes.Length > 0);
}
}
return false;
}
Test Suite:
[TestFixture]
public class WhenInterpolatingATemplateString
{
[TestCase("")]
[TestCase(null)]
public void ThenEmptyValueReturedWhenNoTemplateProvided(string? template)
{
ITemplateInterpolator testInterpolator = new TemplateInterpolator();
var testData = new TestData { Id = 14, Name = "Test" };
var result = testInterpolator.Interpolate(template!, testData);
Assert.That(result, Is.EqualTo(template));
}
[Test]
public void ThenTheTypeNameIsUsedForTheDataReferenceForDefinedClasses()
{
ITemplateInterpolator testInterpolator = new TemplateInterpolator();
var testData = new TestData { Id = 14, Name = "Test" };
string template = "This is a record named \"{TestData.Name}\" with an Id of {testdata.Id}."; // case insensitive.
string expected = "This is a record named \"Test\" with an Id of 14.";
var result = testInterpolator.Interpolate(template, testData);
Assert.That(result, Is.EqualTo(expected));
}
[Test]
public void ThenTheDefaultNameIsUsedForTheDataReferenceForAnonymous()
{
ITemplateInterpolator testInterpolator = new TemplateInterpolator();
var testData = new { Id = 14, Name = "Test" };
string template = "This is a record named \"{data.Name}\" with an Id of {data.Id}.";
string expected = "This is a record named \"Test\" with an Id of 14.";
var result = testInterpolator.Interpolate(template, testData);
Assert.That(result, Is.EqualTo(expected));
}
[Test]
public void ThenTheProvidedDataReferenceNameOverridesTheTypeName()
{
ITemplateInterpolator testInterpolator = new TemplateInterpolator();
var testData = new TestData { Id = 14, Name = "Test" };
string template = "This is a record named \"{otherData.Name}\" with an Id of {otherData.Id}.";
string expected = "This is a record named \"Test\" with an Id of 14.";
var result = testInterpolator.Interpolate(template, testData, "otherData");
Assert.That(result, Is.EqualTo(expected));
}
[Test]
public void ThenExceptionIsThrownWhenTemplateReferencesUnknownDataValues()
{
ITemplateInterpolator testInterpolator = new TemplateInterpolator();
var testData = new TestData { Id = 14, Name = "Test" };
string template = "This is a record named \"{testData.Name}\" with an Id of {testData.Id}. {testData.ExtraDetails}";
Assert.Throws<ParseException>(() => { var result = testInterpolator.Interpolate(template, testData, "testData"); });
}
[Test]
public void ThenDataFormattingExpressionsAreApplied()
{
ITemplateInterpolator testInterpolator = new TemplateInterpolator();
var testData = new { Id = 14, Name = "Test", IsActive = true, EffectiveDate = DateTime.Today };
string template = "The active state is {data.IsActive?\"Yes\":\"No\"}, Effective {data.EffectiveDate.ToString(\"yyyy-MM-dd\")}";
string expected = "The active state is Yes, Effective " + DateTime.Today.ToString("yyyy-MM-dd");
var result = testInterpolator.Interpolate(template, testData);
Assert.That(result, Is.EqualTo(expected));
}
private class TestData
{
public int Id { get; set; }
public string Name { get; set; }
}
}
FormattableString
) is done immediately. – Encephalon