Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Roslyn to parse a lambda expression of a runtime generated type

I'm trying to use Roslyn as a way to parse lambda expressions provided at runtime by users using the general format:

// The first line is normally static and re-used across callers to save perf
Script baseScript = CSharpScript.Create(string.Empty, scriptOptions);
ScriptState<T> scriptState = await baseScript.ContinueWith<Expression<Func<T, bool>>>(code, scriptOptions).RunAsync()
var parsedExpression = scriptState.ReturnValue;

Then the caller provides code as something like P => P.Property > 5. This all works great when I use a well-known type for T, but I'd like to allow users to use more dynamic types, where each user may define their own set of properties (with types). Sync Expression trees don't support dynamic types (and thus Roslyn can't compile as such), I was hoping to allow the user to define their properties and I'd dynamically generate a runtime type.

The problem I'm bumping into is, after creating the runtime type, I don't have a concrete type to use for T in .ContinueWith<Expression<Func<T,bool>>>.

I've using full on reflection doing something like:

var funcType = typeof(Func<,>).MakeGenericType(runtimeType, typeof(bool));
var expressionType = typeof(Expression<>).MakeGenericType(funcType);

var continueWith = script.GetType()
    .GetMethods()
    .Single(m => m.Name == "ContinueWith" && m.IsGenericMethod && m.GetParameters().Any(p => p.ParameterType == typeof(string)))
    .MakeGenericMethod(expressionType);

var lambdaScript = (Script)continueWith.Invoke(script, new object[] { "P => P.String == \"Hi\"", null });
var lambdaScriptState = await lambdaScript.RunAsync();

But this throws an exception:

Microsoft.CodeAnalysis.Scripting.CompilationErrorException: error CS0400: The type or namespace name 'System.Linq.Expressions.Expression1[[System.Func2[[Submission#1+Person, ℛ*907cf320-d303-4781-926e-cee8bf1d3aaf#2-1, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null],[System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' could not be found in the global namespace (are you missing an assembly reference?)

Where in this case, Person is the name of the runtime type (and I think what it's complaining about).

I've tried explicitly adding the assembly the type is in to the ScriptOptions using:

var lambdaScript = (Script)continueWith.Invoke(script, new object[] { "P => P.String == \"Hi\"", scriptOptions.AddReferences(runtimeType.Assembly) });

But this fails with:

System.NotSupportedException: Can't create a metadata reference to an assembly without location.

Is what I'm trying to do possible?

like image 761
Jeff Wight Avatar asked Nov 07 '22 09:11

Jeff Wight


1 Answers

I managed to come up with a solution that worked for what I needed.

For some background, the way I was previously creating the runtimeType was also using Roslyn by doing something like:

Dictionary<string, Type> properties = new Dictionary<string, Type>();
var classDefinition = $"public class Person {{ {string.Join("\n", properties.Select(p => $"public {p.Value.Name} {p.Key};"))} }} return typeof(Person);";

var script = baseScript.ContinueWith<Type>(classDefinition);
var scriptState = await script.RunAsync();
var runtimeType = scriptState.ReturnValue;

Using that example (which I got from some other SO post), I decided to try out just declaring everything together. My updated code now looks something like:

private static readonly string classDefinition = "public class Person { ... }";
Script baseScript = CSharpScript.Create(string.Empty, scriptOptions).ContinueWith(classDefinition);
var scriptState = await baseScript.ContinueWith<Expression>($"Expression<Func<Person,bool>> expression = {code}; return expression;").RunAsync()
var parsedExpression = scriptState.ReturnValue;

The main difference being that with the original, I couldn't simply .ContinueWith<Expression> because it was a lambda expression needed a delegate type, while with the updated version, we declare the actual delegate type within the parsed code, then just do an implicit conversion back to Expression.

I don't actually need the Expression<Func<T,bool>> in my code because we're just looking to pass the Expression into an ExpressionVisitor to re-write all of the property accessors into array accessors (similar to what dynamic does).

like image 120
Jeff Wight Avatar answered Nov 15 '22 06:11

Jeff Wight