Using Regex as a template engine is an efficient way to replace strings in a template. Regex replace method supports executing a method upon match. We can populate a dictionary and make a lookup on all matches. This way we do the replacement in a single pass, rather than executing multiple .Replace(). Since dictionaries can be made case insensitive we have an efficient way of doing case insensitive replacement, something .Replace() does not support.
Example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// Case sensitive match //var data = new Dictionary<string, string>(); // Case insensitive match (using ordinal) var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); // Add all words the template can replace data.Add("WORD1", "Replaced word 1"); data.Add("word2", "Replaced word 2"); var template = "This is a template.\r\nThis is kept: \\[noreplace\\]\r\nThis is not found: [notfound]\r\nReplace 1: [word1]\r\nReplace 2: [WoRd2]"; // Replace any [word] with content from data-dictionary lookup, ignoring escaped \[ and \] // Note that we set a limit of 50 characters on replacement key, just to avoid excessive CPU use on malicious data var content = Regex.Replace(template, @"(?<!\\)\[(?<word>[^\]]{1,50})\](?<!\\)", delegate (Match match) { var word = match.Groups["word"].Value; string ret = null; if (data.TryGetValue(word, out ret)) return ret; return match.ToString(); }); // Replace \[ with [ and \] with ] content = Regex.Replace(content, @"\\([\[\]])", @"$1"); // Print result Console.WriteLine(content); |
I have kept support for escaping [ and ] using \[ and \]. This does add a second pass to the template. Also you may want to add RegexOptions.Compiled (as last parameter on Replace-call, see next example).
Cleaning up the code a bit we see how this can look nice:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
void Main() { // Our template var template = "This is a template.\r\nThis is kept: \\[noreplace\\]\r\nThis is not found: [notfound]\r\nReplace 1: [word1]\r\nReplace 2: [WoRd2]"; // Fill some data var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "WORD1", "Replacement 1" }, { "word2", "Replacement 2" } }; // Convert template var content = TemplateReplace(template, data); // Print result Console.WriteLine(content); } public string TemplateReplace(string template, Dictionary<string, string> data) { // Replace any [word] with content from data-dictionary lookup, ignoring escaped \[ and \] // Note that we set a limit of 50 characters on replacement key, just to avoid excessive CPU use on malicious data var content = Regex.Replace(template, @"(?<!\\)\[(?<word>[^\]]{1,50})\](?<!\\)", delegate (Match match) { var word = match.Groups["word"].Value; string ret = null; if (data.TryGetValue(word, out ret)) return ret; return match.ToString(); }, RegexOptions.Compiled); // Replace \[ with [ and \] with ] content = Regex.Replace(content, @"\\([\[\]])", @"$1", RegexOptions.Compiled); return content; } |
Taking it to the next level we could even populate the replacement data from a model:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
public class SomeModel { public string Name { get; set;} public int PostalCode { get; set;} } void Main() { // Our template var template = "This is a template.\r\nThis is kept: \\[noreplace\\]\r\nThis is not found: [notfound]\r\nReplace 1: [word1]\r\nReplace 2: [WoRd2]\r\nReplace 3: [postalcode] [Name]"; // Set up our replacement data object var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); // Fill some data manually data.Add("WORD1", "Replaced word 1"); data.Add("word2", "Replaced word 2"); // Fill data from another object (model) // We create a sample object var someModel = new SomeModel() { Name = "Tedd", PostalCode = 1364}; // Populate data with properties PopulateObjectIntoDictionary(data, someModel); // Convert template var content = TemplateReplace(template, data); // Print result Console.WriteLine(content); } public void PopulateObjectIntoDictionary(Dictionary<string, string> data, object @object) { // Read the object into the replacement data object using reflection foreach (PropertyInfo pi in @object.GetType().GetProperties()) data[pi.Name] = pi.GetValue(@object, null).ToString(); } public string TemplateReplace(string template, Dictionary<string, string> data) { // Replace any [word] with content from data-dictionary lookup, ignoring escaped \[ and \] // Note that we set a limit of 50 characters on replacement key, just to avoid excessive CPU use on malicious data var content = Regex.Replace(template, @"(?<!\\)\[(?<word>[^\]]{1,50})\](?<!\\)", delegate (Match match) { var word = match.Groups["word"].Value; string ret = null; if (data.TryGetValue(word, out ret)) return ret; return match.ToString(); }, RegexOptions.Compiled); // Replace \[ with [ and \] with ] content = Regex.Replace(content, @"\\([\[\]])", @"$1", RegexOptions.Compiled); return content; } |
The output would be:
1 2 3 4 5 6 |
This is a template. This is kept: [noreplace] This is not found: [notfound] Replace 1: Replaced word 1 Replace 2: Replaced word 2 Replace 3: 1364 Tedd |