Skip to content

Instantly share code, notes, and snippets.

@GuerrillaCoder
Last active August 3, 2017 08:29
Show Gist options
  • Save GuerrillaCoder/c3a140ef759fd04b5307 to your computer and use it in GitHub Desktop.
Save GuerrillaCoder/c3a140ef759fd04b5307 to your computer and use it in GitHub Desktop.
C# Named Value String Format Replace For Templating
// This is a modification of https://mhusseini.wordpress.com/2014/05/03/fast-named-formats-in-c.
// To make it work as a flexible templating system I modified it so that it can use dynamic objects
// and silently removes unused fields.
// I am not sure how this has effected its performance.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Text.RegularExpressions;
namespace StringExtension
{
public static class NamedFormat
{
// cache
private static readonly ConcurrentDictionary<string, object> PrecompiledExpressions = new ConcurrentDictionary<string, object>(StringComparer.OrdinalIgnoreCase);
private static readonly Regex RegexFormatArgs = new Regex(@"([^{]|^){(\w+)}([^}]|$)|([^{]|^){(\w+)\:(.+)}([^}]|$)", RegexOptions.Compiled);
public static string Format<T>(string pattern, T item)
{
var cacheKey = item.GetType().GetHashCode() + pattern;
// If we already have a compiled expression, just execute it.
object o;
if (PrecompiledExpressions.TryGetValue(cacheKey, out o))
{
return ((Func<T, string>)o)(item);
}
bool isIDictionary = (item is IDictionary<string, object>);
// Make an array of all property names to be checked against template
string[] keyArray =
isIDictionary
? ((IDictionary<string, object>)item).Keys.Select(x => x.ToString()).ToArray<string>()
: item.GetType().GetProperties().Select(x => x.Name).ToArray<string>();
// Convert named format into regular format and return
// a list of the named arguments in order of appearance.
string replacedPattern;
var arguments = ParsePattern(pattern, out replacedPattern, keyArray);
// Now, construct code with Linq Expressions...
// We'll be using the String.Format method to actually perform the formating.
var formatMethod = typeof(string).GetMethod("Format", new[] { typeof(string), typeof(object[]) });
// The constant that contains the format string:
var patternExpression = Expression.Constant(replacedPattern, typeof(string));
// If its a non-dynamic class we can access object properties without conversion
var parameterExpression = Expression.Parameter(typeof(T), "static class");
// If its a IDictionary then we need to perorm conversion so we can access value like a property
var castToDic = Expression.Convert(parameterExpression, typeof(IDictionary<string, object>));
// create an array of property accessors
var argumentArrayElements = isIDictionary
? arguments.Select(argument => Expression.Convert(Expression.Property(castToDic, "Item", Expression.Constant(argument)), typeof(object))) // arguments.Select(s => ((IDictionary<string, object>)d)[s])
: arguments.Select(argument => Expression.Convert(Expression.Property(parameterExpression, argument), typeof(object)));
var argumentArrayExpressions = Expression.NewArrayInit(typeof(object), argumentArrayElements);
// The actual call to String.Format:
var formatCallExpression = Expression.Call(formatMethod, patternExpression, argumentArrayExpressions);
// The lambda expression we will be compiling:
var lambdaExpression = Expression.Lambda<Func<T, string>>(formatCallExpression, parameterExpression);
// The lambda expression will look something like this
// input => string.Format("my format string", new[]{ input.Arg0, input.Arg1, ... }); or
// input => string.Format("my format string", new[]{ ((IDictionary<string,object>)input).Item.Arg0, ((IDictionary<string,object>)input).Item.Arg0})
// Now we can compile the lambda expression
var func = lambdaExpression.Compile();
// Cache the pre-compiled expression use type hash & pattern
PrecompiledExpressions.TryAdd(item.GetType().GetHashCode() + pattern, func);
// Execute the compiled expression
return func(item);
}
private static IEnumerable<string> ParsePattern(string pattern, out string replacedPattern, string[] objectKeys)
{
// Just replace each named format items with regular format items
// and put all named format items in a list. Then return the
// new format string and the list of the named items.
var sb = new StringBuilder();
var lastIndex = 0;
var arguments = new List<string>();
var lowerarguments = new List<string>();
foreach (var @group in from Match m in RegexFormatArgs.Matches(pattern)
select m.Groups[m.Groups[6].Success ? 5 : 2])
{
var key = @group.Value;
var lkey = key.ToLowerInvariant();
var index = lowerarguments.IndexOf(lkey);
if (index < 0)
{
index = lowerarguments.Count;
}
// if it is not in array silently remove it
if (!objectKeys.Contains(lkey, StringComparer.InvariantCultureIgnoreCase))
{
sb.Append(pattern.Substring(lastIndex, (@group.Index - 1) - lastIndex));
lastIndex = @group.Index + (@group.Length + 1);
}
// otherwise replace it with index and add to arguments
else
{
lowerarguments.Add(lkey);
arguments.Add(key);
sb.Append(pattern.Substring(lastIndex, @group.Index - lastIndex));
sb.Append(index);
lastIndex = @group.Index + @group.Length;
}
}
sb.Append(pattern.Substring(lastIndex));
replacedPattern = sb.ToString();
return arguments;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment