19
votes

Given this class

public class Foo
{
    public string Name { get; set; }
}

This method (in some other class)...

private Func<Foo, string> Compile(string body)
{
    ParameterExpression prm = Expression.Parameter(typeof(Foo), "foo");
    LambdaExpression exp = DynamicExpressionParser.ParseLambda(new[] { prm }, typeof(string), body);
    return (Func<Foo, string>)exp.Compile();
}

Will take the right hand side of a lambda expression and give me back a delegate. So if it is called like this:

Foo f = new Foo { Name = "Hamilton Academicals" };
//foo => foo.Name.Substring(0,3)
Func<Foo, string> fn = Compile("foo.Name.Substring(0,3)");
string sub = fn(f);

Then sub will have the value "Ham".

All well and good, however, I would like to make Foo subclass DynamicObject (so that I can implement TryGetMember to dynamically work out property values), so I want to take the expression and get the equivalent of this

Func<dynamic, dynamic> fn = foo => foo.Name.Substring(0,3);

I've tried Expression.Dynamic using a custom CallSiteBinder, but that fails with No property or field Bar exists in type Object (when I try to access foo.Bar dynamically). I'm assuming that is because the call to get foo.Bar needs to be dynamically dispatched (using Expression.Dynamic), but that isn't going to work for me because a key aim is that a user can enter a simple expression and have it executed. Is it possible?

1
For that I assume you need another parser which works specifically with dynamic expressions. This one is not going to work. Maybe just use full blown compiler (Roslyn)? How complex expressions are expected? - Evk
@Evk - Yep. After I posted I came to the same conclusion. Expressions will be simple (one liners). Can't use Roslyn as the generated assemblies aren't eligible for garbage collection. - user2729292
You can load generated assemblies into separate app domains and tear them down. Of course that will hit perfomance and in general not very nice solution, but might be better than nothing, depending on your requirements to perfomance. - Evk
True... except I want to target .NET Core - user2729292
Well, then you are out of luck I think, at least until they implement dll unloading in .NET Core (that's upcoming feature as far as I know). Implementing your own parser which works with dynamic expressions might be quite not trivial. - Evk

1 Answers

1
votes

I got this working, if it'll help you out:

Compiler:

using System;
using System.Linq.Dynamic.Core;
using System.Linq.Expressions;


namespace ConsoleApp4
{
    public class Compiler
    {
        public static Func<CustomDynamic, TResult> Compile<TResult>(string body)
        {
            ParameterExpression prm = Expression.Parameter(typeof(CustomDynamic), typeof(CustomDynamic).Name);
            LambdaExpression exp = DynamicExpressionParser.ParseLambda(new[] { prm }, typeof(TResult), body);
            return (Func<CustomDynamic, TResult>)exp.Compile();
        }
    }
}

Dynamic Object:

using System.Collections.Generic;
using System.Dynamic;

namespace ConsoleApp4
{
    public class CustomDynamic
    {
        ExpandoObject _values;
        public CustomDynamic()
        {
            _values = new ExpandoObject();
        }

        public void AddProperty(string propertyName, object propertyValue)
        {
            var expandoDict = _values as IDictionary<string, object>;
            if (expandoDict.ContainsKey(propertyName))
                expandoDict[propertyName] = propertyValue;
            else
                expandoDict.Add(propertyName, propertyValue);
        }

        public string GetString(string propertyName)
        {
            var expandoDict = _values as IDictionary<string, object>;
            if (expandoDict.ContainsKey(propertyName))
                return (string)expandoDict[propertyName];
            else
                throw new KeyNotFoundException($"dynamic object did not contain property {propertyName}");
        }
        public int GetInt(string propertyName)
        {
            var expandoDict = _values as IDictionary<string, object>;
            if (expandoDict.ContainsKey(propertyName))
                return (int)expandoDict[propertyName];
            else
                throw new KeyNotFoundException($"dynamic object did not contain property {propertyName}");
        }
    }
}

Use Case:

using System;

namespace ConsoleApp4
{
    class Program
    {
        static void Main(string[] args)
        {
            CustomDynamic f = new CustomDynamic();
            f.AddProperty("Name", "Hamiltonian Physics");

            Func<CustomDynamic, string> fn = Compiler.Compile<string>("CustomDynamic.GetString(\"Name\").SubString(0, 3)");

            string sub = fn(f);

            Console.WriteLine(sub);
            Console.ReadLine();
        }
    }
}

It just utilises the ExpandoObject class. It's unfortunate that you'd need to go through its API to create objects, like "AddProperty" for every property, but heyho.

The DynamicExpressionParser.ParseLambda is a bit of a pain when it comes to generic methods as well, so I had to create type-specific accessors, which isn't the best, but it's working.