Dynamic string interpolation
Asked Answered
O

15

40

Can anyone help me with this?

Required Output: "Todo job for admin"

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(ReplaceMacro("{job.Name} job for admin", new Job { Id = 1, Name = "Todo", Description="Nothing" }));
        Console.ReadLine();
    }

    static string ReplaceMacro(string value, Job job)
    {
        return value; //Output should be "Todo job for admin"
    }
}

class Job
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}
Oswell answered 5/10, 2016 at 12:36 Comment(4)
You can't, basically. That's not how interpolated string literals work. The string formatting (or conversion to FormattableString) is done immediately.Encephalon
Though there's nothing stopping you from just implementing a naive solution using string replacements. Depending on the circumstances it may or may not have performance issues, but you can do it.Janae
You'd have to do your own parsing of the string.Bizerte
I think what you're asking for is a FormattableString and is the object that is output by the compiler for an interpolated string. Search for the Interpolated strings: advanced usages - Meziantou's blog for a good illustration of one technique that derives a class from FormattableString to do what you're asking.Discretionary
B
48

Two suggestions:

DataBinder.Eval

string ReplaceMacro(string value, Job job)
{
    return Regex.Replace(value, @"{(?<exp>[^}]+)}", match => {
        return (System.Web.UI.DataBinder.Eval(new { Job = job }, match.Groups["exp"].Value) ?? "").ToString();
    });
}

Linq.Expression

Use the Dynamic Query class provided in the MSDN LINQSamples:

string ReplaceMacro(string value, Job job)
{
    return Regex.Replace(value, @"{(?<exp>[^}]+)}", match => {
        var p = Expression.Parameter(typeof(Job), "job");
        var e = System.Linq.Dynamic.DynamicExpression.ParseLambda(new[] { p }, null, match.Groups["exp"].Value);
        return (e.Compile().DynamicInvoke(job) ?? "").ToString();
    });
}

In my opinion, the Linq.Expression is more powerful, so if you trust the input string, you can do more interesting things, i.e.:

value = "{job.Name.ToUpper()} job for admin"
return = "TODO job for admin"
Bearce answered 6/10, 2016 at 16:2 Comment(5)
could you please update regex for "${job.Name}"...ThanksOswell
Perfect. Daft thing was, I was already using dynamic expressions for more complex things, and using regex replacing of trivial variables, but I didn't put 2+2 together to use regex to find complex variable names and then use my existing expression parsing code to replace them...Lafleur
Thanks!, I wasted two day on thisFuriya
Those of you looking for a .NET Core version can utilize the System.Linq.Dynamic.Core library. The ParseLambda line must then be changed to: var e = System.Linq.Dynamic.Core.DynamicExpressionParser.ParseLambda(new[] { p }, null, match.Groups["exp"].Value);Elizaelizabet
This is great! Exactly what I was looking for. Storing a complex string in my database and "interpolating" out with runtime data. The Linq Expression is extremely powerful allowing for deep nested objects' replacement, and @Nigel's Core version plugged in like a champ too! Thanks all!Grabble
V
19

You can't use string interpolation this way. But you can still use the pre-C#6 way to do it using string.Format:

static void Main(string[] args)
{
    Console.WriteLine(ReplaceMacro("{0} job for admin", new Job { Id = 1, Name = "Todo", Description = "Nothing" }));
    Console.ReadLine();
}

static string ReplaceMacro(string value, Job job)
{
    return string.Format(value, job.Name);
}
Victorinavictorine answered 5/10, 2016 at 12:42 Comment(2)
As string interpolation is just syntactic sugar and String.Format is still used in result IL, it's really hard to do anything more fancy here.Aphesis
This should essentially be the answer as it is concise and does exactly what is required, alongside being simple and easy to understand;Joletta
S
5

This generic solution Extend the answer provided by @Dan
It can be used for any typed object.

install System.Linq.Dynamic

     Install-Package System.Linq.Dynamic -Version 1.0.7 

    string ReplaceMacro(string value, object @object)
    {
        return Regex.Replace(value, @"{(.+?)}", 
        match => {
            var p = Expression.Parameter(@object.GetType(), @object.GetType().Name);                
            var e = System.Linq.Dynamic.DynamicExpression.ParseLambda(new[] { p }, null, match.Groups[1].Value);
            return (e.Compile().DynamicInvoke(@object) ?? "").ToString();
        });
    }

See a working demo for a Customer type

Seaden answered 4/1, 2019 at 0:1 Comment(0)
Q
3

Wrap the string in a function...

var f = x => $"Hi {x}";

f("Mum!");

//... Hi Mum!
Quadrivalent answered 18/8, 2019 at 21:10 Comment(0)
N
3

You could use RazorEngine:

using RazorEngine;

class Program 
{
    static void Main(string[] args)
    {
        Console.WriteLine(ReplaceMacro("@Model.Name job for admin", new Job { Id = 1, Name = "Todo", Description="Nothing" }));
        Console.ReadLine();
    }

    static string ReplaceMacro(string value, Job job)
    {
        return Engine.Razor.RunCompile(value, "key", typeof(Job), job);
    }
}

It even supports Anonymous Types and method calls:

string template = "Hello @Model.Name. Today is @Model.Date.ToString(\"MM/dd/yyyy\")";
var model = new { Name = "Matt", Date = DateTime.Now };

string result = Engine.Razor.RunCompile(template, "key", null, model);
Nickinickie answered 10/4, 2020 at 17:45 Comment(1)
For .net core projects consider RazorEngine.NetCore and not RazorEngineBulbiferous
I
3

Little late to the party! Here is the one I wrote -

using System.Reflection;
using System.Text.RegularExpressions;

public static class AmitFormat
{
    //Regex to match keywords of the format {variable}
    private static readonly Regex TextTemplateRegEx = new Regex(@"{(?<prop>\w+)}", RegexOptions.Compiled);

    /// <summary>
    /// Replaces all the items in the template string with format "{variable}" using the value from the data
    /// </summary>
    /// <param name="templateString">string template</param>
    /// <param name="model">The data to fill into the template</param>
    /// <returns></returns>
    public static string FormatTemplate(this string templateString, object model)
    {
        if (model == null)
        {
            return templateString;
        }

        PropertyInfo[] properties = model.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);

        if (!properties.Any())
        {
            return templateString;
        }

        return TextTemplateRegEx.Replace(
            templateString,
            match =>
            {
                PropertyInfo property = properties.FirstOrDefault(propertyInfo =>
                    propertyInfo.Name.Equals(match.Groups["prop"].Value, StringComparison.OrdinalIgnoreCase));

                if (property == null)
                {
                    return string.Empty;
                }

                object value = property.GetValue(model, null);

                return value == null ? string.Empty : value.ToString();
            });
    }
}

Example -

string format = "{foo} is a {bar} is a {baz} is a {qux} is a really big {fizzle}";
var data = new { foo = 123, bar = true, baz = "this is a test", qux = 123.45, fizzle = DateTime.UtcNow };

Compared with other implementations given by Phil Haack and here are the results for the above example -

AmitFormat    took 0.03732 ms
Hanselformat  took 0.09482 ms
OskarFormat   took 0.1294 ms
JamesFormat   took 0.07936 ms
HenriFormat   took 0.05024 ms
HaackFormat   took 0.05914 ms
Integrity answered 21/10, 2020 at 10:0 Comment(2)
that's a very solid implementation. I am just wondering how this works with a formatted string like in your example how {qux:#.#} getting converted to 123.45. Regex won't even match the expression. No?Tympany
Right, it doesn't match, above code does pattern match-and-replace. Removed the :#.#.Integrity
J
2

Not exactly but with bit tweek, I have created generic interpolation which support fields / property only.

public static string Interpolate(this string template, params Expression<Func<object, string>>[] values)
        {
            string result = template;
            values.ToList().ForEach(x =>
            {
                MemberExpression member = x.Body as MemberExpression;
                string oldValue = $"{{{member.Member.Name}}}";
                string newValue = x.Compile().Invoke(null).ToString();
                result = result.Replace(oldValue, newValue);
            }

                );
            return result;
        }

Test case

 string jobStr = "{Name} job for admin";
        var d = new { Id = 1, Name = "Todo", Description = "Nothing" };
        var result = jobStr.Interpolate(x => d.Name);

Another

            string sourceString = "I wanted abc as {abc} and {dateTime} and {now}";
        var abc = "abcIsABC";
        var dateTime = DateTime.Now.Ticks.ToString();
        var now = DateTime.Now.ToString();
        string result = sourceString.Interpolate(x => abc, x => dateTime, x => now);
Judaic answered 22/1, 2019 at 12:48 Comment(0)
B
1

You need named string format replacement. See Phil Haack's post from years ago: http://haacked.com/archive/2009/01/04/fun-with-named-formats-string-parsing-and-edge-cases.aspx/

Brechtel answered 14/6, 2017 at 12:39 Comment(0)
L
1

Wondering no one has mentioned mustache-sharp. Downloadable via Nuget.

string templateFromSomewhere = "url: {{Url}}, Name:{{Name}}";
FormatCompiler compiler = new FormatCompiler();
Generator generator = compiler.Compile(templateFromSomewhere);
string result = generator.Render(new
{
    Url="https://google.com",
    Name = "Bob",
});//"url: https://google.com, Name:Bob"

More examples could be found here, at the unit testing file.

Luxury answered 27/9, 2021 at 7:23 Comment(1)
Just tried Mustache# for a similar problem and I must say it is a great lib. Very simple, uses standard c# formatting convention and has much more features than its readme.md suggest. Thanks for pointing it.Lucila
A
1

Starting from the accepted answer I created a generic extension method:

public static string Replace<T>(this string template, T value)
{
    return Regex.Replace(template, @"{(?<exp>[^}]+)}", match => {
        var p = Expression.Parameter(typeof(T), typeof(T).Name);
        var e = System.Linq.Dynamic.Core.DynamicExpressionParser.ParseLambda(new[] { p }, null, match.Groups["exp"].Value);
        return (e.Compile().DynamicInvoke(value) ?? "").ToString();
    });
}
Abstractionism answered 10/1, 2022 at 9:43 Comment(3)
I came across that accepted answer when looking for a replacement to some ugly specific string replacement for eMail templates and created a similar generic implementation. Though I added an optional string for the value property name so that the template might reference a meaningful known name other than the type name passed, or allow the method to parse data from something like an anonymous type. string Interpolate<TData>(string template, TData data, string? interpolateDataAs = null) where if not provided a name, try using TData type name or "data".Alkahest
It works? Can you provide the code?Abstractionism
Added the implementation down below.Alkahest
B
0

Answer from @ThePerplexedOne is better, but if you really need to avoid string interpolation, so

static string ReplaceMacro(string value, Job job)
{
    return value?.Replace("{job.Name}", job.Name); //Output should be "Todo job for admin"
}
Bunkmate answered 5/10, 2016 at 12:42 Comment(0)
K
0

You should change your function to:

static string ReplaceMacro(Job obj, Func<dynamic, string> function)
{
    return function(obj);
}

And call it:

Console.WriteLine(
    ReplaceMacro(
        new Job { Id = 1, Name = "Todo", Description = "Nothing" },
        x => $"{x.Name} job for admin"));
Keitel answered 5/10, 2016 at 12:45 Comment(0)
S
0

If you really need this, you can do it using Roslyn, create string – class implementation like

var stringToInterpolate = "$@\"{{job.Name}} job for admin\"";
var sourceCode = $@"
using System;
class RuntimeInterpolation(Job job) 
{{
    public static string Interpolate() =>
        {stringToInterpolate};
}}";

then make an assembly

var assembly = CompileSourceRoslyn(sourceCode);
var type = assembly.GetType("RuntimeInterpolation");
var instance = Activator.CreateInstance(type);
var result = (string) type.InvokeMember("Interpolate",
BindingFlags.Default | BindingFlags.InvokeMethod, null, instance, new object[] {new Job { Id = 1, Name = "Todo", Description="Nothing" }});
Console.WriteLine(result);

you'll get this code by runtime and you'll get your result (also you'll need to attach link to your job class to that assembly)

using System;
class RuntimeInterpolation(Job job) 
{
    public static string Interpolate() =>
        $@"{job.Name} job for admin";
}

You can read about how to implement CompileSourceRoslyn here Roslyn - compiling simple class: "The type or namespace name 'string' could not be found..."

Sheeran answered 19/7, 2020 at 21:55 Comment(3)
fix: var stringToInterpolate = "$@\"{job.Name} job for admin\"";Sheeran
or you can pass only name to method so it will be var stringToInterpolate = "$@\"{name} job for admin\""; class RuntimeInterpolation(string name) {{ public static string Interpolate() => {stringToInterpolate}; }}";Sheeran
If you have additional information or code to add to your answer, please click edit under your answer to modify it. Code is very difficult to read in comments, and comments might get deleted.Easley
A
0

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; }
    }

}
Alkahest answered 19/9, 2022 at 21:22 Comment(0)
N
-1

I really don't understand the point of your ReplaceMacro method...

But here's how it should work:

class Program
{
    static void Main(string[] args)
    {
        var job = new Job { Id = 1, Name = "Todo", Description = "Nothing" };
        Console.WriteLine($"{job.Name} job for admin");
        Console.ReadLine();
    }
}

But if you really want the dynamic feel to it, your ReplaceMacro method should just take one parameter, which is the job:

static string ReplaceMacro(Job job)
{
    return $"{job.Name} job for admin.";
}

And use it like:

var job = new Job { Id = 1, Name = "Todo", Description = "Nothing" };
Console.WriteLine(ReplaceMacro(job));

Or something to that effect.

News answered 5/10, 2016 at 12:41 Comment(2)
I think the idea is that the string might be read in from a configuration file and then the OP wants to apply the interpolation dynamically.Bizerte
@orenrevenge Why do you write responses to a 5 year old question?News

© 2022 - 2024 — McMap. All rights reserved.