暇になったので小ネタ、Expressionから値が欲しいとき(´д`)

System.Linq.ExpressionをVisitしたりコネコネしたりする時のお話(・ω・)


Expressionを使ってなにをするかといえば、.NETの構文からクエリ言語を生成するだとか、IQueryableもどきみたいなことをするケースが多いと思いますが(`・ω・´) *1
っで、ExpressionをVisitしながらターゲットを構築していく途中で、Expressionを評価した値が欲しくなるケースがあると思います。


要は、

Where(Expression<Func<TEntity, bool>> filter)

みたいなメソッドを作って、その中でクエリを構築するようなケースで、

Query<Data>.Where(_ => _.No >= param.Min && _.No <= param.Max)

っと書いた時に、param.Minやparam.Maxの値をクエリ構築中に評価したい、っというような話なわけですが(・ω・) *2


っで、値が欲しい場合、Expression.Lambda(expression).Compile().DynamicInvoke()すれば取得できるんですが、これは死ぬほど遅いので多用するのはちょっと考えないといけないわけで(´д`;)
でも、いくつかのケースではCompile()せずに、値を高速に取得ができるので、その方法について以下ソース。

public static object EvalExpression(Expression expression)
{
    // Constant
    if (expression.NodeType == ExpressionType.Constant)
    {
        return ((ConstantExpression)expression).Value;
    }

    // MemberAccess
    if (expression.NodeType == ExpressionType.MemberAccess)
    {
        var member = (MemberExpression)expression;

        var target = member.Expression != null ? EvalExpression(member.Expression) : null;
        var pi = member.Member as PropertyInfo;
        if (pi != null)
        {
            return pi.GetValue(target, null);
        }
        var fi = member.Member as FieldInfo;
        if (fi != null)
        {
            return fi.GetValue(target);
        }
    }

    // Call
    if (expression.NodeType == ExpressionType.Call)
    {
        var call = (MethodCallExpression)expression;

        var target = call.Object != null ? EvalExpression(call.Object) : null;
        return call.Method.Invoke(target, call.Arguments.Select(EvalExpression).ToArray());
    }

    // 遅いので未サポートにしておく
    //return Expression.Lambda(expression).Compile().DynamicInvoke();
    throw new NotSupportedException("Unsupported expression.");
}

まず、定数値についてはConstantExpressionのValueから値を取得できるのでそれを使用。
フィールド及びプロパティからの取得については、MemberExpression.Memberで取得できるMemberInfoを使ってリフレクションを使って処理という感じ。


DSLを構築するようなケースだと、値を評価したいケースなんて、定数かメンバからの値取得が大半なので、上記だけ高速になれば十分な気はしますが(・ω・)
複雑な式の評価値が欲しいようなケースでは事前に一時変数に代入してしまえばMemberExpressionで処理できるわけで、ここまでにしても良いんですが(・∀・;)


でもまあ、単純なメソッド呼び出しくらいもサポートしておこうかしら、っというわけで、MethodCallExpressionの処理も追加しておきました。
ただし、宣言として、メソッド呼び出しの引数として上記の処理内で解決できないような式を書くことは未サポートということで。


使用イメージは以下。

// Expressionからなにかしらを構築していく処理の例
static void EvalExpression(Expression<Func<object>> expression)
{
    Debug.WriteLine(expression.Body);
}

とかにたいして、

// Constant
Test(() => 1);

// MemberAccess
var a = 1;
Test(() => a);

// MemberAccess
var hoge = new Hoge()
Test(() => hoge.Value);

// Call
Test(() => hoge.GetValue());

みたいなものがExpression.Lambda(expression).Compile()に比べて高速に処理できるようになりますた(`・ω・´)


なんか1年近く日記を書いていませんでしたが、中途半端に暇になったので小ネタを書いてみますた(・ω・;)

*1:要はDSLの構築。

*2:式木全体の評価はExpressionVisitorで処理。