I have a JSON configuration file where the user is allowed to use inside strings certain placeholders {Type.Name} that later will be interpolated with the actual values (for connection strings, titles, texts, summaries etc.).
For example in report settings he might want to use the TestCase.Severityvalue
"Title": "{TestCase.Severity}",
Each variable consists of the name of the declaring type and the name of the property.
I did not want to hardcode those properties (there are a lot more of them) so I build a VariableBuilder. It uses a simple member and getter expression to generate names and resolve values. The variables are stored in a dictionary keyed with the declaring type and a hash-set that keeps a type variables.
internal class VariableBuilder
{
private readonly IDictionary<Type, HashSet<INameable>> _variables = new Dictionary<Type, HashSet<INameable>>();
public void AddVariable<T>(Expression<Func<T, object>> expression)
{
if (expression.Body is MemberExpression memberExpression)
{
var variableName = $"{memberExpression.Member.DeclaringType.Name}.{memberExpression.Member.Name}";
var instance = Expression.Parameter(typeof(T), "obj");
var property = Expression.Property(instance, memberExpression.Member.Name);
var getValue = Expression.Lambda<Func<T, object>>(property, instance).Compile();
if (_variables.TryGetValue(typeof(T), out var variables))
{
if (!variables.Add(Variable<T>.Create(variableName, getValue)))
{
throw new ArgumentException($"Variable \"{variableName}\" has already been added.");
}
}
else
{
_variables.Add(typeof(T), new HashSet<INameable> { Variable<T>.Create(variableName, getValue) });
}
}
else
{
throw new ArgumentException("Expression must be a member expression.");
}
}
public IEnumerable<KeyValuePair<string, object>> ResolveVariables<T>(T obj)
{
if (_variables.TryGetValue(typeof(T), out var variables))
{
foreach (var variable in variables.Cast<Variable<T>>())
{
yield return new KeyValuePair<string, object>(variable.Name, variable.GetValue(obj));
}
}
}
}
To prevent name conflicts I use the INameable interface that requires the Variable<T> to also implement the IEquatable interface so that the hash-set can do its job right.
internal interface INameable : IEquatable<INameable>
{
string Name { get; }
}
The actual variable name and the getter func are stored by the Variable<T>.
internal class Variable<T> : INameable
{
private Variable(string name, Func<T, object> getValue)
{
Name = name;
GetValue = getValue;
}
public string Name { get; }
public Func<T, object> GetValue { get; }
public static INameable Create(string name, Func<T, object> getValue)
{
return new Variable<T>(name, getValue);
}
public bool Equals(INameable nameable)
{
return Name.Equals(nameable.Name);
}
public override int GetHashCode() => Name.GetHashCode();
}
Here are a few examples of how I use it. When a test is run and a report should be generated I pass the actual test objects to the variable-builder that resolves the final values and later during report generation another utility (VariableResolver) can interpolate them into strings.
var variableReader = new VariableBuilder();
variableReader.AddVariable<TestFile>(x => x.Name);
//variableReader.AddVariable<TestFile>(x => x.Name); // throws because of duplicte key
variableReader.AddVariable<TestUnit>(x => x.Filter);
var testFile = new TestFile { Name = "foo" };
var testUnit = new TestUnit { Filter = "bar" };
var testFileVariables = variableReader.ResolveVariables(testFile);
var testUnitVariables = variableReader.ResolveVariables(testUnit);
The two types used in the above example (those are not some test objects, the names are real but the actual objects just have a few more properties):
internal class TestFile
{
public string Name { get; set; }
}
internal class TestUnit
{
public string Filter { get; set; }
}