76

I need to send email notifications to users and I need to allow the admin to provide a template for the message body (and possibly headers, too).

I'd like something like string.Format that allows me to give named replacement strings, so the template can look like this:

Dear {User},

Your job finished at {FinishTime} and your file is available for download at {FileURL}.

Regards,

-- 
{Signature}

What's the simplest way for me to do that?

Simon
  • 25,468
  • 44
  • 152
  • 266

12 Answers12

51

Here is the version for those of you who can use a new version of C#:

// add $ at start to mark string as template
var template = $"Your job finished at {FinishTime} and your file is available for download at {FileURL}."

In a line - this is now a fully supported language feature (string interpolation).

Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • 46
    Note that this is a compile-time substitution, essentially sugar around `String.Format()`, and doesn't help if you are obtaining the template string at run time. +1 for the best way to do it at compile-time, though. – DaveRandom Jan 08 '16 at 14:45
  • 2
    You can just extract the code to a function and then pass the items inside as parameters - this is _as_ expressive as other templating techniques. – Benjamin Gruenbaum Jul 12 '16 at 07:18
  • 2
    @DaveRandom: the [docs](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/interpolated-strings) say otherwise: `The interpolated string is evaluated each time the code with the interpolated string executes. This allows you to separate the definition and evaluation of an interpolated string.` Or am I reading this wrong? – Martijn Jun 29 '17 at 19:31
  • 8
    @Martijn sorry that was poorly phrased, what I meant is that the template itself is defined at compile-time, and it's not possible to e.g. let the user alter the template. The compiler effectively generates a `string.Format()` substitution, and while it is possible to let a user redefine those, they aren't particularly user friendly because they can no longer use *named* values in the template. What I was after when I arrived at this question was something like the solution I eventually wrote for my answer. – DaveRandom Jul 01 '17 at 14:04
  • 1
    If you are loading your template from an external template file, this will not work – James Poulose Jan 16 '21 at 03:46
  • Worth mentioning that if you want to escape `{`, then `\{` **WON'T** work. Use `{{`. See https://stackoverflow.com/questions/91362/how-to-escape-braces-curly-brackets-in-a-format-string-in-net –  Sep 24 '21 at 19:14
  • we cannot use this as a separate text file like template engine? – cikatomo Nov 09 '22 at 23:07
29

You can use the "string.Format" method:

var user = GetUser();
var finishTime = GetFinishTime();
var fileUrl = GetFileUrl();
var signature = GetSignature();
string msg =
@"Dear {0},

Your job finished at {1} and your file is available for download at {2}.

Regards,

--
{3}";
msg = string.Format(msg, user, finishTime, fileUrl, signature);

It allows you to change the content in the future and is friendly for localization.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
TcKs
  • 25,849
  • 11
  • 66
  • 104
  • That only allows numbered inputs, and throws an exception if the user puts in too high a number. – Simon Apr 09 '09 at 12:59
  • 1
    Yes only numbered inputs, ok. But "throws an exception if the user puts in too high a number" is not true. I tried it with "1024 * 1024" input parameters and it works. – TcKs Apr 09 '09 at 13:09
  • No, I mean if you replaced {3} with {4} but don't provide another input parameter. – Simon Apr 09 '09 at 13:26
  • 5
    Yes, but ANY solution ought to produce an exception if the template contains symbols that aren't being replaced. It's far better to shed some light on the fact that something is wrong than to bury your head in the sand! – The Dag Jan 24 '12 at 09:52
  • +1 This is actually the best answer because it doesn't require an external lib and its not only useful at compile time. – Mark Rogers Jul 12 '16 at 01:13
28

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());
dwb
  • 2,136
  • 13
  • 27
Anton Gogolev
  • 113,561
  • 39
  • 200
  • 288
  • 7
    I would be great if samples page could contain any example of usage. Link to C# samples page: https://theantlrguy.atlassian.net/wiki/display/ST4/Using+StringTemplate+with+CSharp – Marek Bar Nov 05 '14 at 07:45
  • 2
    The atlassian.net link above requires logging in. Try this link instead to see examples of StringTemplate: https://theantlrguy.atlassian.net/wiki/spaces/ST/pages/1409116/Five+minute+Introduction – Ryan Shripat Apr 27 '18 at 17:37
18

I wrote a pretty simple library, SmartFormat which meets all your requirements. It is focused on composing "natural language" text, and is great for generating data from lists, or applying conditional logic.

The syntax is extremely similar to String.Format, and is very simple and easy to learn and use. Here's an example of the syntax from the documentation:

Smart.Format("{Name}'s friends: {Friends:{Name}|, |, and}", user)
// Result: "Scott's friends: Michael, Jim, Pam, and Dwight"

The library has great error-handling options (ignore errors, output errors, throw errors) and is open source and easily extensible, so you can also enhance it with additional features too.

Ian Kemp
  • 28,293
  • 19
  • 112
  • 138
Scott Rippey
  • 15,614
  • 5
  • 70
  • 85
  • 1
    Thanks for this handy library. This is what I went with after reviewing many alternatives. There is also a [Nuget package](https://www.nuget.org/packages/SmartFormat.NET/) for it. – orad Jul 03 '14 at 23:53
  • 1
    This one's pretty awesome if you don't need advanced things like partials. Especially the feature that you can swap `string.Format` with `Smart.Format` so you can gradually evolve your templates. Thanks! – mfeineis Jun 10 '15 at 16:12
17

Building on Benjamin Gruenbaum's answer, in C# version 6 you can add a @ with the $ and pretty much use your code as it is, e.g.:

var text = $@"Dear {User},

Your job finished at {FinishTime} and your file is available for download at {FileURL}.

Regards,

-- 
{Signature}
";

The $ is for string interpolation: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/interpolated

The @ is the verbatim identifier: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/verbatim

...and you can use these in conjunction.

:o)

mrrrk
  • 2,186
  • 20
  • 17
  • 8
    String interpolation only works during compilation, so you can't get the string templates from external source like config file or DB. – Ekus Feb 28 '20 at 16:36
9

A very simple regex-based solution. Supports \n-style single character escape sequences and {Name}-style named variables.

Source

class Template
{
    /// <summary>Map of replacements for characters prefixed with a backward slash</summary>
    private static readonly Dictionary<char, string> EscapeChars
        = new Dictionary<char, string>
        {
            ['r'] = "\r",
            ['n'] = "\n",
            ['\\'] = "\\",
            ['{'] = "{",
        };

    /// <summary>Pre-compiled regular expression used during the rendering process</summary>
    private static readonly Regex RenderExpr = new Regex(@"\\.|{([a-z0-9_.\-]+)}",
        RegexOptions.IgnoreCase | RegexOptions.Compiled);

    /// <summary>Template string associated with the instance</summary>
    public string TemplateString { get; }

    /// <summary>Create a new instance with the specified template string</summary>
    /// <param name="TemplateString">Template string associated with the instance</param>
    public Template(string TemplateString)
    {
        if (TemplateString == null) {
            throw new ArgumentNullException(nameof(TemplateString));
        }

        this.TemplateString = TemplateString;
    }

    /// <summary>Render the template using the supplied variable values</summary>
    /// <param name="Variables">Variables that can be substituted in the template string</param>
    /// <returns>The rendered template string</returns>
    public string Render(Dictionary<string, object> Variables)
    {
        return Render(this.TemplateString, Variables);
    }

    /// <summary>Render the supplied template string using the supplied variable values</summary>
    /// <param name="TemplateString">The template string to render</param>
    /// <param name="Variables">Variables that can be substituted in the template string</param>
    /// <returns>The rendered template string</returns>
    public static string Render(string TemplateString, Dictionary<string, object> Variables)
    {
        if (TemplateString == null) {
            throw new ArgumentNullException(nameof(TemplateString));
        }

        return RenderExpr.Replace(TemplateString, Match => {
            switch (Match.Value[0]) {
                case '\\':
                    if (EscapeChars.ContainsKey(Match.Value[1])) {
                        return EscapeChars[Match.Value[1]];
                    }
                    break;

                case '{':
                    if (Variables.ContainsKey(Match.Groups[1].Value)) {
                        return Variables[Match.Groups[1].Value].ToString();
                    }
                    break;
            }

            return string.Empty;
        });
    }
}

Usage

var tplStr1 = @"Hello {Name},\nNice to meet you!";
var tplStr2 = @"This {Type} \{contains} \\ some things \\n that shouldn't be rendered";
var variableValues = new Dictionary<string, object>
{
    ["Name"] = "Bob",
    ["Type"] = "string",
};

Console.Write(Template.Render(tplStr1, variableValues));
// Hello Bob,
// Nice to meet you!

var template = new Template(tplStr2);
Console.Write(template.Render(variableValues));
// This string {contains} \ some things \n that shouldn't be rendered

Notes

  • I've only defined \n, \r, \\ and \{ escape sequences and hard-coded them. You could easily add more or make them definable by the consumer.
  • I've made variable names case-insensitive, as things like this are often presented to end-users/non-programmers and I don't personally think that case-sensitivity make sense in that use-case - it's just one more thing they can get wrong and phone you up to complain about (plus in general if you think you need case sensitive symbol names what you really need are better symbol names). To make them case-sensitive, simply remove the RegexOptions.IgnoreCase flag.
  • I strip invalid variable names and escape sequences from the result string. To leave them intact, return Match.Value instead of the empty string at the end of the Regex.Replace callback. You could also throw an exception.
  • I've used {var} syntax, but this may interfere with the native interpolated string syntax. If you want to define templates in string literals in you code, it might be advisable to change the variable delimiters to e.g. %var% (regex \\.|%([a-z0-9_.\-]+)%) or some other syntax of your choosing which is more appropriate to the use case.
DaveRandom
  • 87,921
  • 11
  • 154
  • 174
  • I prefer case-sensitive templates by default. Beside removing `RegexOptions.IgnoreCase` I had to add group `A-Z` to the `RenderExpr`, too. – xmedeko Mar 25 '19 at 09:18
8

You could use string.Replace(...), eventually in a for-each through all the keywords. If there are only a few keywords you can have them on a line like this:

string myString = template.Replace("FirstName", "John").Replace("LastName", "Smith").Replace("FinishTime", DateTime.Now.ToShortDateString());

Or you could use Regex.Replace(...), if you need something a bit more powerful and with more options.

Read this article on codeproject to view which string replacement option is fastest for you.

Ovi
  • 2,620
  • 7
  • 34
  • 51
  • 2
    It's not good way, if you have a lot of parameters to replace and/or have long string - because string is immutable, so there is big memory usage. – TcKs Apr 09 '09 at 09:41
  • +1 @TcKs - agreed, the example has 4 parameters so I'd go with @Ovi's suggestion, but if it got to around 10 or so then I'd look to a templating language.. – Andrew Apr 09 '09 at 09:56
  • This could use lots of memory, but a templating engine or language could very well be overkill also. – Steven Evers Apr 09 '09 at 18:10
  • The Garbage Collector will address some relatives of yours if you know what I mean... – Andrei Rînea Apr 10 '09 at 00:10
  • 1
    All of the above is true, but irrelevant for many applications (where the templates are small and milliseconds don't matter). If the solution does not need to be *generic* and it's OK to disallow any of the template's *literal* text to ever look like a symbol (for instance by disallowing '{' and '}' in literal text), this may actually be enough. On the other hand it is very easy to improve the performance side with the same functionality as a simple replace. – The Dag Jan 24 '12 at 09:49
5

In case someone is searching for an alternative -- an actual .NET one:

https://github.com/crozone/FormatWith | https://www.nuget.org/packages/FormatWith

A nice simple extendable solution. Thank you crozone!

So using the string extension provided in FormatWith here are two examples:

    static string emailTemplate = @"
Dear {User},

Your job finished at {FinishTime} and your file is available for download at {FileURL}.

-- 
{Signature}
    ";

//////////////////////////////////
/// 1. Use a dictionary that has the tokens as keys with values for the replacement
//////////////////////////////////
    public void TestUsingDictionary()
    {    
        var emailDictionary = new Dictionary<string, object>()
        {
            { "User", "Simon" },
            { "FinishTime", DateTime.Now },
            { "FileUrl", new Uri("http://example.com/dictionary") },
            { "Signature", $"Sincerely,{Environment.NewLine}Admin" }
        };

        var emailBody = emailTemplate.FormatWith(emailDictionary);

        System.Console.WriteLine(emailBody);
    }

//////////////////////////////////
/// 2. Use a poco with properties that match the replacement tokens
//////////////////////////////////
    public class MessageValues
    {
        public string User { get; set; } = "Simon";
        public DateTime FinishTime { get; set; } = DateTime.Now;
        public Uri FileURL { get; set; } = new Uri("http://example.com");
        public string Signature { get; set; } = $"Sincerely,{Environment.NewLine}Admin";
    }

    public void TestUsingPoco()
    {
        var emailBody = emailTemplate.FormatWith(new MessageValues());

        System.Console.WriteLine(emailBody);
    }


It allows formatting the replacement inline as well. For example, try changing {FinishTime} to {FinishTime:HH:mm:ss} in emailTemplate.

jimnkey
  • 372
  • 2
  • 10
  • 1
    I really would like to use it, but char as a brace make it totally useless for me. I hope for string as brace support in the future. – Tomasz Kaniewski Feb 02 '21 at 11:03
4

Actually, you can use XSLT. You create a simple XML template:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
  <xsl:template match="TETT">
    <p>
       Dear <xsl:variable name="USERNAME" select="XML_PATH" />,

       Your job finished at <xsl:variable name="FINISH_TIME" select="XML_PATH" /> and your file is available for download at <xsl:variable name="FILE_URL" select="XML_PATH" />.

       Regards,
        -- 
       <xsl:variable name="SIGNATURE" select="XML_PATH" />
    </p>
</xsl:template>

Then create a XmlDocument to perform transformation against: XmlDocument xmlDoc = new XmlDocument();

        XmlNode xmlNode = xmlDoc .CreateNode(XmlNodeType.Element, "EMAIL", null);
        XmlElement xmlElement= xmlDoc.CreateElement("USERNAME");
        xmlElement.InnerXml = username;
        xmlNode .AppendChild(xmlElement); ///repeat the same thing for all the required fields

        xmlDoc.AppendChild(xmlNode);

After that, apply the transformation:

        XPathNavigator xPathNavigator = xmlDocument.DocumentElement.CreateNavigator();
        StringBuilder sb = new StringBuilder();
        StringWriter sw = new StringWriter(sb);
        XmlTextWriter xmlWriter = new XmlTextWriter(sw);
        your_xslt_transformation.Transform(xPathNavigator, null, xmlWriter);
        return sb.ToString();
0100110010101
  • 6,469
  • 5
  • 33
  • 38
4

Implementing your own custom formatter might be a good idea.

Here's how you do it. First, create a type that defines the stuff you want to inject into your message. Note: I'm only going to illustrate this with the User part of your template...

class JobDetails
{
    public string User 
    { 
        get;
        set; 
    }        
}

Next, implement a simple custom formatter...

class ExampleFormatter : IFormatProvider, ICustomFormatter
{
    public object GetFormat(Type formatType)
    {
        return this;
    }

    public string Format(string format, object arg, IFormatProvider formatProvider)
    {
        // make this more robust
        JobDetails job = (JobDetails)arg;

        switch (format)
        {
            case "User":
            {
                return job.User;
            }
            default:
            {
                // this should be replaced with logic to cover the other formats you need
                return String.Empty;
            }
        }
    }
}

Finally, use it like this...

string template = "Dear {0:User}. Your job finished...";

JobDetails job = new JobDetails()
                     {
                             User = "Martin Peck"
                     };

string message = string.Format(new ExampleFormatter(), template, job);

... which will generate the text "Dear Martin Peck. Your job finished...".

Martin Peck
  • 11,440
  • 2
  • 42
  • 69
0

If you are coding in VB.NET you can use XML literals. If you are coding in C# you can use ShartDevelop to have files in VB.NET in the same project as C# code.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
epitka
  • 17,275
  • 20
  • 88
  • 141
0

If you need something very powerful (but really not the simplest way) you can host ASP.NET and use it as your templating engine.

You'll have all the power of ASP.NET to format the body of your message.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
thinkbeforecoding
  • 6,668
  • 1
  • 29
  • 31