Bagaimana cara mengubah sebuah String ke Pohon Ekspresi LINQ yang setara?

173

Ini adalah versi sederhana dari masalah aslinya.

Saya memiliki kelas yang disebut Person:

public class Person {
  public string Name { get; set; }
  public int Age { get; set; }
  public int Weight { get; set; }
  public DateTime FavouriteDay { get; set; }
}

... dan katakanlah sebuah instance:

var bob = new Person {
  Name = "Bob",
  Age = 30,
  Weight = 213,
  FavouriteDay = '1/1/2000'
}

Saya ingin menulis yang berikut sebagai string di editor teks favorit saya ....

(Person.Age > 3 AND Person.Weight > 50) OR Person.Age < 3

Saya ingin mengambil string ini dan contoh objek saya dan mengevaluasi BENAR atau SALAH - yaitu mengevaluasi Func <Person, bool> pada instance objek.

Inilah pemikiran saya saat ini:

  1. Menerapkan tata bahasa dasar di ANTLR untuk mendukung Operator Perbandingan dan Logika dasar. Saya berpikir untuk menyalin prioritas Visual Basic dan beberapa fitur di sini: http://msdn.microsoft.com/en-us/library/fw84t893(VS.80).aspx
  2. Minta ANTLR membuat AST yang cocok dari string yang disediakan.
  3. Berjalan AST dan gunakan kerangka kerja Predicate Builder untuk secara dinamis membuat Func <Person, bool>
  4. Mengevaluasi predikat terhadap instance Person sesuai kebutuhan

Pertanyaan saya adalah apakah saya benar-benar overbaked ini? ada alternatif?


EDIT: Solusi Terpilih

Saya memutuskan untuk menggunakan Dynamic Linq Library, khususnya kelas Dynamic Query yang disediakan di LINQSamples.

Kode di bawah ini:

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

namespace ExpressionParser
{
  class Program
  {
    public class Person
    {
      public string Name { get; set; }
      public int Age { get; set; }
      public int Weight { get; set; }
      public DateTime FavouriteDay { get; set; }
    }

    static void Main()
    {
      const string exp = @"(Person.Age > 3 AND Person.Weight > 50) OR Person.Age < 3";
      var p = Expression.Parameter(typeof(Person), "Person");
      var e = System.Linq.Dynamic.DynamicExpression.ParseLambda(new[] { p }, null, exp);
      var bob = new Person
      {
        Name = "Bob",
        Age = 30,
        Weight = 213,
        FavouriteDay = new DateTime(2000,1,1)
      };

      var result = e.Compile().DynamicInvoke(bob);
      Console.WriteLine(result);
      Console.ReadKey();
    }
  }
}

Hasilnya adalah tipe System.Boolean, dan dalam hal ini BENAR.

Banyak terima kasih kepada Marc Gravell.

Termasuk System.Linq. Paket nuget dinamis, dokumentasi di sini

Codebrain
sumber
33
Terima kasih telah memposting kode solusi lengkap bersama dengan pertanyaan Anda. Sangat dihargai.
Adrian Grigore
bagaimana jika Anda memiliki koleksi atau orang dan ingin memfilter beberapa elemen? Person.Age> 3 AND Person. Weight> 50?
serhio
Terima kasih. Saya tidak dapat menemukan DynamicExpression.ParseLambda (). Namespace dan assembly mana yang ada?
Matt Fitzmaurice
Semua baik .. Ada ambiguitas antara ruang nama. Diperlukan - menggunakan E = System.Linq.Expressions; menggunakan System.Linq.Dynamic;
Matt Fitzmaurice
Mengapa menggunakan 'DAN' bukannya '&&'. Bukankah seharusnya kode C #?
Triynko

Jawaban:

65

Apakah pustaka linq dinamis membantu di sini? Secara khusus, saya berpikir sebagai Whereklausa. Jika perlu, letakkan di dalam daftar / larik hanya untuk memanggilnya .Where(string)! yaitu

var people = new List<Person> { person };
int match = people.Where(filter).Any();

Jika tidak, menulis parser (menggunakan di Expressionbawah tenda) tidak sangat melelahkan - saya menulis yang serupa (walaupun saya tidak berpikir saya memiliki sumber) di kereta saya pergi sebelum xmas ...

Marc Gravell
sumber
Tandai apa yang Anda maksud dengan "menulis parser (menggunakan Ekspresi di bawah tenda)" "Parsing dan kemudian menghasilkan pohon ekspresi, atau apakah System.Linq.Expressions memiliki mekanisme parsing?
AK_
Saya cukup yakin bahwa dia ingin membaca dalam file dengan ekspresi yang dibentuk seperti itu dan kemudian diterjemahkan sebagai predikat dan dikompilasi. Pertanyaan tampaknya adalah, membuat tata bahasa Anda dikonversi dari 'string' ke 'predikat'. // Lambda expression as data in the form of an expression tree. System.Linq.Expressions.Expression<Func<int, bool>> expr = i => i < 5; // Compile the expression tree into executable code. Func<int, bool> deleg = expr.Compile(); // Invoke the method and print the output. Console.WriteLine("deleg(4) = {0}", deleg(4)); ParseLambda bagus!
Latency
31

Perpustakaan lain yang serupa adalah Kabur

Saya melakukan perbandingan cepat Dynamic Linq Library dan Flee and Flee 10 kali lebih cepat untuk ekspresi"(Name == \"Johan\" AND Salary > 500) OR (Name != \"Johan\" AND Salary > 300)"

Ini cara Anda dapat menulis kode menggunakan Flee.

static void Main(string[] args)
{
  var context = new ExpressionContext();
  const string exp = @"(Person.Age > 3 AND Person.Weight > 50) OR Person.Age < 3";
  context.Variables.DefineVariable("Person", typeof(Person));
  var e = context.CompileDynamic(exp);

  var bob = new Person
  {
    Name = "Bob",
    Age = 30,
    Weight = 213,
    FavouriteDay = new DateTime(2000, 1, 1)
  };

  context.Variables["Person"] = bob;
  var result = e.Evaluate();
  Console.WriteLine(result);
  Console.ReadKey();
}
chikak
sumber
Mungkin saya kehilangan sesuatu, tetapi bagaimana 'lari' membantu dalam membangun pohon ekspresi LINQ?
Michael B Hildebrand
9
void Main()
{
    var testdata = new List<Ownr> {
        //new Ownr{Name = "abc", Qty = 20}, // uncomment this to see it getting filtered out
        new Ownr{Name = "abc", Qty = 2},
        new Ownr{Name = "abcd", Qty = 11},
        new Ownr{Name = "xyz", Qty = 40},
        new Ownr{Name = "ok", Qty = 5},
    };

    Expression<Func<Ownr, bool>> func = Extentions.strToFunc<Ownr>("Qty", "<=", "10");
    func = Extentions.strToFunc<Ownr>("Name", "==", "abc", func);

    var result = testdata.Where(func.ExpressionToFunc()).ToList();

    result.Dump();
}

public class Ownr
{
    public string Name { get; set; }
    public int Qty { get; set; }
}

public static class Extentions
{
    public static Expression<Func<T, bool>> strToFunc<T>(string propName, string opr, string value, Expression<Func<T, bool>> expr = null)
    {
        Expression<Func<T, bool>> func = null;
        try
        {
            var type = typeof(T);
            var prop = type.GetProperty(propName);
            ParameterExpression tpe = Expression.Parameter(typeof(T));
            Expression left = Expression.Property(tpe, prop);
            Expression right = Expression.Convert(ToExprConstant(prop, value), prop.PropertyType);
            Expression<Func<T, bool>> innerExpr = Expression.Lambda<Func<T, bool>>(ApplyFilter(opr, left, right), tpe);
            if (expr != null)
                innerExpr = innerExpr.And(expr);
            func = innerExpr;
        }
        catch (Exception ex)
        {
            ex.Dump();
        }

        return func;
    }
    private static Expression ToExprConstant(PropertyInfo prop, string value)
    {
        object val = null;

        try
        {
            switch (prop.Name)
            {
                case "System.Guid":
                    val = Guid.NewGuid();
                    break;
                default:
                    {
                        val = Convert.ChangeType(value, prop.PropertyType);
                        break;
                    }
            }
        }
        catch (Exception ex)
        {
            ex.Dump();
        }

        return Expression.Constant(val);
    }
    private static BinaryExpression ApplyFilter(string opr, Expression left, Expression right)
    {
        BinaryExpression InnerLambda = null;
        switch (opr)
        {
            case "==":
            case "=":
                InnerLambda = Expression.Equal(left, right);
                break;
            case "<":
                InnerLambda = Expression.LessThan(left, right);
                break;
            case ">":
                InnerLambda = Expression.GreaterThan(left, right);
                break;
            case ">=":
                InnerLambda = Expression.GreaterThanOrEqual(left, right);
                break;
            case "<=":
                InnerLambda = Expression.LessThanOrEqual(left, right);
                break;
            case "!=":
                InnerLambda = Expression.NotEqual(left, right);
                break;
            case "&&":
                InnerLambda = Expression.And(left, right);
                break;
            case "||":
                InnerLambda = Expression.Or(left, right);
                break;
        }
        return InnerLambda;
    }

    public static Expression<Func<T, TResult>> And<T, TResult>(this Expression<Func<T, TResult>> expr1, Expression<Func<T, TResult>> expr2)
    {
        var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());
        return Expression.Lambda<Func<T, TResult>>(Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters);
    }

    public static Func<T, TResult> ExpressionToFunc<T, TResult>(this Expression<Func<T, TResult>> expr)
    {
        var res = expr.Compile();
        return res;
    }
}

LinqPad memiliki Dump()metode

suneelsarraf
sumber
dimana metode GetProperty?
Alen.Toma
@ Alen.Toma Saya harus mengubah kode var type = typeof(T); var prop = type.GetProperty(propName);untuk mendapatkannya untuk dikompilasi.
Giles Roberts
Membuatnya mengkompilasi dan membuang output
Amit
5

Anda mungkin melihat DLR . Ini memungkinkan Anda untuk mengevaluasi dan menjalankan skrip di dalam aplikasi .NET 2.0. Berikut ini contoh dengan IronRuby :

using System;
using IronRuby;
using IronRuby.Runtime;
using Microsoft.Scripting.Hosting;

class App
{
    static void Main()
    {
        var setup = new ScriptRuntimeSetup();
        setup.LanguageSetups.Add(
            new LanguageSetup(
                typeof(RubyContext).AssemblyQualifiedName,
                "IronRuby",
                new[] { "IronRuby" },
                new[] { ".rb" }
            )
        );
        var runtime = new ScriptRuntime(setup);
        var engine = runtime.GetEngine("IronRuby");
        var ec = Ruby.GetExecutionContext(runtime);
        ec.DefineGlobalVariable("bob", new Person
        {
            Name = "Bob",
            Age = 30,
            Weight = 213,
            FavouriteDay = "1/1/2000"
        });
        var eval = engine.Execute<bool>(
            "return ($bob.Age > 3 && $bob.Weight > 50) || $bob.Age < 3"
        );
        Console.WriteLine(eval);

    }
}

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public int Weight { get; set; }
    public string FavouriteDay { get; set; }
}

Tentu saja teknik ini didasarkan pada evaluasi runtime dan kode tidak dapat diverifikasi pada waktu kompilasi.

Darin Dimitrov
sumber
1
Saya ingin dapat melindungi dari eksekusi 'kode buruk' ... apakah ini cocok?
Codebrain
Apa yang Anda maksud dengan 'kode buruk'? Seseorang mengetik ekspresi yang tidak valid? Dalam hal ini Anda akan mendapatkan pengecualian runtime ketika mencoba mengevaluasi skrip.
Darin Dimitrov
@darin, hal-hal seperti memulai proses, mengubah data, dll.
sisve
2
'kode buruk' = sesuatu yang bukan ekspresi tipe Func <Person, bool> (mis. menghapus file dari disk, memutar proses dll ...)
Codebrain
1

Berikut ini adalah contoh parser combinator berbasis Scala DSL untuk penguraian dan evaluasi ekspresi aritmatika.

import scala.util.parsing.combinator._
/** 
* @author Nicolae Caralicea
* @version 1.0, 04/01/2013
*/
class Arithm extends JavaTokenParsers {
  def expr: Parser[List[String]] = term ~ rep(addTerm | minusTerm) ^^
    { case termValue ~ repValue => termValue ::: repValue.flatten }

  def addTerm: Parser[List[String]] = "+" ~ term ^^
    { case "+" ~ termValue => termValue ::: List("+") }

  def minusTerm: Parser[List[String]] = "-" ~ term ^^
    { case "-" ~ termValue => termValue ::: List("-") }

  def term: Parser[List[String]] = factor ~ rep(multiplyFactor | divideFactor) ^^
    {
      case factorValue1 ~ repfactor => factorValue1 ::: repfactor.flatten
    }

  def multiplyFactor: Parser[List[String]] = "*" ~ factor ^^
    { case "*" ~ factorValue => factorValue ::: List("*") }

  def divideFactor: Parser[List[String]] = "/" ~ factor ^^
    { case "/" ~ factorValue => factorValue ::: List("/") }

  def factor: Parser[List[String]] = floatingPointConstant | parantExpr

  def floatingPointConstant: Parser[List[String]] = floatingPointNumber ^^
    {
      case value => List[String](value)
    }

  def parantExpr: Parser[List[String]] = "(" ~ expr ~ ")" ^^
    {
      case "(" ~ exprValue ~ ")" => exprValue
    }

  def evaluateExpr(expression: String): Double = {
    val parseRes = parseAll(expr, expression)
    if (parseRes.successful) evaluatePostfix(parseRes.get)
    else throw new RuntimeException(parseRes.toString())
  }
  private def evaluatePostfix(postfixExpressionList: List[String]): Double = {
    import scala.collection.immutable.Stack

    def multiply(a: Double, b: Double) = a * b
    def divide(a: Double, b: Double) = a / b
    def add(a: Double, b: Double) = a + b
    def subtract(a: Double, b: Double) = a - b

    def executeOpOnStack(stack: Stack[Any], operation: (Double, Double) => Double): (Stack[Any], Double) = {
      val el1 = stack.top
      val updatedStack1 = stack.pop
      val el2 = updatedStack1.top
      val updatedStack2 = updatedStack1.pop
      val value = operation(el2.toString.toDouble, el1.toString.toDouble)
      (updatedStack2.push(operation(el2.toString.toDouble, el1.toString.toDouble)), value)
    }
    val initial: (Stack[Any], Double) = (Stack(), null.asInstanceOf[Double])
    val res = postfixExpressionList.foldLeft(initial)((computed, item) =>
      item match {
        case "*" => executeOpOnStack(computed._1, multiply)
        case "/" => executeOpOnStack(computed._1, divide)
        case "+" => executeOpOnStack(computed._1, add)
        case "-" => executeOpOnStack(computed._1, subtract)
        case other => (computed._1.push(other), computed._2)
      })
    res._2
  }
}

object TestArithmDSL {
  def main(args: Array[String]): Unit = {
    val arithm = new Arithm
    val actual = arithm.evaluateExpr("(12 + 4 * 6) * ((2 + 3 * ( 4 + 2 ) ) * ( 5 + 12 ))")
    val expected: Double = (12 + 4 * 6) * ((2 + 3 * ( 4 + 2 ) ) * ( 5 + 12 ))
    assert(actual == expected)
  }
}

Pohon ekspresi setara atau pohon parse dari ekspresi aritmatika yang disediakan akan menjadi tipe Parser [Daftar [String]].

Lebih detail ada di tautan berikut:

http://nicolaecaralicea.blogspot.ca/2013/04/scala-dsl-for-parsing-and-evaluating-of.html

ncaralicea
sumber
0

Selain Dynamic Linq Library (yang membangun ekspresi yang sangat diketik dan membutuhkan variabel yang sangat diketik) saya menyarankan alternatif yang lebih baik: linq parser bagian dari NReco Commons Library (open source). Ini meluruskan semua jenis dan melakukan semua doa saat runtime dan berperilaku seperti bahasa dinamis:

var lambdaParser = new NReco.LambdaParser();
var varContext = new Dictionary<string,object>();
varContext["one"] = 1M;
varContext["two"] = "2";

Console.WriteLine( lambdaParser.Eval("two>one && 0<one ? (1+8)/3+1*two : 0", varContext) ); // --> 5
Vitaliy Fedorchenko
sumber