Every now and then we get asked if there’s an easy way to parse user input into filter conditions. Say, for example, we have a viewmodel of type DataThing
:
public class DataThing
{
public string Name;
public float Value;
public int Count;
}
From here we’d like to check if a given property of this class satisfies a certain condition. For example, we’ll look at “Value is greater than 15”. But of course, we’d like to be flexible.
The issue
The main issue here is we don’t know the type of property beforehand, so we can’t use generics even if we try to be smart:
public class DataThing
{
public string Name;
public float Value;
public int Count;
}
public static void Main()
{
var data = new DataThing() {Value=10, Name="test", Count = 1};
var values = new List {
new ValueGetter(x => x.Value),
new ValueGetter(x => x.Name)
};
(values[0].Run(data) > 15).Dump();
}
public abstract class ValueGetter
{
public abstract T Run<T>(DataThing d);
}
public class ValueGetter<T> : ValueGetter
{
public Func<DataThing, T> TestFunc;
public ValueGetter(Func<DataThing, T> blah)
{
TestFunc = blah;
}
public override T Run(DataThing d) => TestFunc.Invoke(d); // CS0029 Cannot implicitly convert type…
}
Even if we figured it out it’s obviously way too dependant on DataThing
layout to be used everywhere.
LINQ Expression trees
One way to solve this issue is with the help of LINQ expression trees. This way we wrap everything into one delegate with predictable signature and figure out types at runtime:
bool BuildComparer(DataThing data, string field, string op, T value) {
var p1 = Expression.Parameter(typeof(DataThing));
var p2 = Expression.Parameter(typeof(T));
if (op == ">")
{
var expr = Expression.Lambda>(
Expression.MakeBinary(ExpressionType.GreaterThan
, Expression.PropertyOrField(p1, field)
, Expression.Convert(p2, typeof(T))), p1, p2);
var f = expr.Compile();
return f(data, value);
<span style="background-color: inherit; font-family: monospace; font-size: inherit;">} </span>
<span style="background-color: inherit; font-family: monospace; font-size: inherit;">return false;</span>
}
Code DOM CSharpScript
Another way to approach the same problem is to generate C# code that we can compile and run. We’d need Microsoft.CodeAnalysis.CSharp.Scripting
package for this to work:
bool BuildScript(DataThing data, string field, string op, T value)
{
var code = $"return {field} {op} {value};";
var script = CSharpScript.Create(code, globalsType: typeof(DataThing), options: ScriptOptions.Default);
var scriptRunner = script.CreateDelegate();
return scriptRunner(data).Result;
}
.NET 5 Code Generator
This is a new .NET 5 feature, that allows us to plug into compilation process and generate classes as we see fit. For example, we’d generate extension methods that would all return correct values from DataThing
:
[Generator] // see https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md for even more cool stuff
class AccessorGenerator: ISourceGenerator {
public void Execute(GeneratorExecutionContext context) {
var syntaxReceiver = (CustomSyntaxReceiver) context.SyntaxReceiver;
ClassDeclarationSyntax userClass = syntaxReceiver.ClassToAugment;
SourceText sourceText = SourceText.From($ @ "
public static class DataThingExtensions {
{
// This is where we'd reflect over type members and generate code dynamically. Following code is oversimplification
public static string GetValue<string>(this DataThing d) => d.Name;
public static string GetValue<float>(this DataThing d) => d.Value;
public static string GetValue<int>(this DataThing d) => d.Count;
}
}
", Encoding.UTF8);
context.AddSource("DataThingExtensions.cs", sourceText);
}
public void Initialize(GeneratorInitializationContext context) {
context.RegisterForSyntaxNotifications(() => new CustomSyntaxReceiver());
}
class CustomSyntaxReceiver: ISyntaxReceiver {
public ClassDeclarationSyntax ClassToAugment {
get;
private set;
}
public void OnVisitSyntaxNode(SyntaxNode syntaxNode) {
// Business logic to decide what we're interested in goes here
if (syntaxNode is ClassDeclarationSyntax cds &&
cds.Identifier.ValueText == "DataThing") {
ClassToAugment = cds;
}
}
}
}
Running this should be as easy as calling extension methods on the class instance: data.GreaterThan(15f).Dump();
Top comments (0)