views:

46

answers:

3

I could've sworn I saw some articles a while ago about imperfect but useful string interpolation methods for C, but no such luck right now. However, there's Razor, which does more or less what I want.

Suppose you have a database client application with tickets, and e-mail notifications are to be sent whenever tickets get created, significant parameters change, etc. The user would like to customize the wording of those notification e-mails, which would be easiest using string interpolation, i.e. accessing various properties of the ticket object from within the string, like so:

Dear @user,

the ticket @ticket.ID (@ticket.URL) has changed in priority from @previousTicket.priority to @currentTicket.priority.

What I'd like is a method that I pass various objects (in this case user, oldTicket and ticket), and have it evaluate the string and get the necessary properties through reflection.

+1  A: 

While I'm sure there's many engines out there that do this, we settled on Castle NVelocity, and it does it really well.

http://www.castleproject.org/others/nvelocity/usingit.html

It accepts the data via name/value pairs, and runs it through a template. It can be used for generating all kinds of textual output in memory. It supports includes, conditional sections, and also repeating data (eg. lines on an order).

Most importantly, it's damn easy to use.

Will
Very interesting. I'll have to try that out as well. Thanks. :)
Sören Kuklau
+2  A: 

You can use a simple replacement step to achieve a simple keyword replacement functionality.

Just replace your keywords with {0}, {1}, etc and use string.Format with the right parameter in the right place.

Dictionary<string, int> keywords = new Dictionary<string, int>();
keywords["@user"] = 0;
keywords["@ticket.ID"] = 1;
keywords["@ticket.URL"] = 2;
// etc...
string template = @"Dear @user,

the ticket @ticket.ID (@ticket.URL) has changed in priority from @previousTicket.priority to @currentTicket.priority.";

string replacedTemplate = template;
foreach (var keyword in keywords)
{
    replacedTemplate = replacedTemplate.Replace(keyword.Key, "{" + keyword.Value + "}");
}
string formattedMessage = string.Format(replacedTemplate, userName, ticket.ID, ticket.URL); // corresponding to the dictionary

This assumes that you have a well defined and limited amount of keywords.

Albin Sunnanbo
I like this, it uses KISS principle...
Vnuk
And the string can't execute arbitrary code
CodeInChaos
A: 

I hadn't seen the two answers, so I went ahead and did my own implementation:

using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace StringInterpolation {
    /// <summary>
    /// An object with an explicit, available-at-runtime name.
    /// </summary>
    public struct NamedObject {
        public string Name;
        public object Object;

        public NamedObject(string name, object obj) {
            Name = name;
            Object = obj;
        }
    }

    public static class StringInterpolation {
        /// <summary>
        /// Parses a string for basic Razor-like interpolation with explicitly passed objects.
        /// For example, pass a NamedObject user, and you can use @user and @user.SomeProperty in your string.
        /// </summary>
        /// <param name="s">The string to be parsed.</param>
        /// <param name="objects">A NamedObject array for objects too allow for parsing.</param>
        public static string Interpolate(this string s, params NamedObject[] objects) {
            System.Diagnostics.Debug.WriteLine(s);

            List<NamedObject> namedObjects = new List<NamedObject>(objects);

            Dictionary<NamedObject, Dictionary<string, string>> objectsWithProperties = new Dictionary<NamedObject, Dictionary<string, string>>();

            foreach (NamedObject no in objects) {
                Dictionary<string, string> properties = new Dictionary<string, string>();

                foreach (System.Reflection.PropertyInfo pInfo in no.Object.GetType().GetProperties())
                    properties.Add(pInfo.Name, pInfo.GetValue(no.Object, new object[] { }).ToString());

                objectsWithProperties.Add(no, properties);
            }

            foreach (Match match in Regex.Matches(s, @"@(\w+)(\.(\w+))?")) {
                NamedObject no;
                no = namedObjects.Find(delegate(NamedObject n) { return n.Name == match.Groups[1].Value; });

                if (no.Name != null && match.Groups.Count == 4)
                    if (string.IsNullOrEmpty(match.Groups[3].Value))
                        s = s.Replace(match.Value, no.Object.ToString());
                    else {
                        Dictionary<string, string> properties = null;
                        string value;
                        objectsWithProperties.TryGetValue(no, out properties);

                        if (properties != null && properties.TryGetValue(match.Groups[3].Value, out value))
                            s = s.Replace(match.Value, value);
                    }

            }

            return s;
        }
    }
}

And here's a test:

using StringInterpolation;

namespace StringInterpolationTest {
    class User {
        public string Name { get; set; }
    }

    class Ticket {
        public string ID { get; set; }
        public string Priority { get; set; }
    }

    class Program {
        static void Main(string[] args) {
            User user = new User();
            user.Name = "Joe";
            Ticket previousTicket = new Ticket();
            previousTicket.ID = "1";
            previousTicket.Priority = "Low";
            Ticket currentTicket = new Ticket();
            currentTicket.ID = "1";
            currentTicket.Priority = "High";

            System.Diagnostics.Debug.WriteLine("User: @user, Username: @user.Name, Previous ticket priority: @previousTicket.Priority, New priority: @currentTicket.Priority".Interpolate(
                new NamedObject("user", user),
                new NamedObject("previousTicket", previousTicket),
                new NamedObject("currentTicket", currentTicket)
            ));
        }
    }
}

It's quite a bit more code than Albin's variant, but doesn't require manually setting up IDs (though it still needs you to know ahead of time which objects to 'export' for potential interpolation).

Thanks guys!

Sören Kuklau