Menggabungkan dua ekspresi (Ekspresi <Func <T, bool >>)

249

Saya memiliki dua ekspresi tipe Expression<Func<T, bool>>dan saya ingin mengambil OR, DAN atau TIDAK dari ini dan mendapatkan ekspresi baru dari tipe yang sama

Expression<Func<T, bool>> expr1;
Expression<Func<T, bool>> expr2;

...

//how to do this (the code below will obviously not work)
Expression<Func<T, bool>> andExpression = expr AND expr2
BjartN
sumber
8
Posting yang sangat berguna yang saya dapatkan dari Google: LINQ ke Entitas: Menggabungkan Predikat
Thomas CG de Vilhena

Jawaban:

331

Nah, Anda bisa menggunakan Expression.AndAlso/ OrElseetc untuk menggabungkan ekspresi logis, tetapi masalahnya adalah parameternya; apakah Anda bekerja dengan yang sama ParameterExpressiondi expr1 dan expr2? Jika demikian, lebih mudah:

var body = Expression.AndAlso(expr1.Body, expr2.Body);
var lambda = Expression.Lambda<Func<T,bool>>(body, expr1.Parameters[0]);

Ini juga berfungsi dengan baik untuk meniadakan satu operasi:

static Expression<Func<T, bool>> Not<T>(
    this Expression<Func<T, bool>> expr)
{
    return Expression.Lambda<Func<T, bool>>(
        Expression.Not(expr.Body), expr.Parameters[0]);
}

Jika tidak, tergantung pada penyedia LINQ, Anda mungkin dapat menggabungkannya dengan Invoke:

// OrElse is very similar...
static Expression<Func<T, bool>> AndAlso<T>(
    this Expression<Func<T, bool>> left,
    Expression<Func<T, bool>> right)
{
    var param = Expression.Parameter(typeof(T), "x");
    var body = Expression.AndAlso(
            Expression.Invoke(left, param),
            Expression.Invoke(right, param)
        );
    var lambda = Expression.Lambda<Func<T, bool>>(body, param);
    return lambda;
}

Di suatu tempat, saya sudah mendapatkan beberapa kode yang menulis ulang pohon ekspresi-menggantikan node untuk menghapus kebutuhan Invoke, tetapi itu cukup panjang (dan saya tidak ingat di mana saya meninggalkannya ...)


Versi umum yang memilih rute paling sederhana:

static Expression<Func<T, bool>> AndAlso<T>(
    this Expression<Func<T, bool>> expr1,
    Expression<Func<T, bool>> expr2)
{
    // need to detect whether they use the same
    // parameter instance; if not, they need fixing
    ParameterExpression param = expr1.Parameters[0];
    if (ReferenceEquals(param, expr2.Parameters[0]))
    {
        // simple version
        return Expression.Lambda<Func<T, bool>>(
            Expression.AndAlso(expr1.Body, expr2.Body), param);
    }
    // otherwise, keep expr1 "as is" and invoke expr2
    return Expression.Lambda<Func<T, bool>>(
        Expression.AndAlso(
            expr1.Body,
            Expression.Invoke(expr2, param)), param);
}

Mulai dari .NET 4.0, ada ExpressionVisitorkelas yang memungkinkan Anda untuk membangun ekspresi yang aman EF.

    public static Expression<Func<T, bool>> AndAlso<T>(
        this Expression<Func<T, bool>> expr1,
        Expression<Func<T, bool>> expr2)
    {
        var parameter = Expression.Parameter(typeof (T));

        var leftVisitor = new ReplaceExpressionVisitor(expr1.Parameters[0], parameter);
        var left = leftVisitor.Visit(expr1.Body);

        var rightVisitor = new ReplaceExpressionVisitor(expr2.Parameters[0], parameter);
        var right = rightVisitor.Visit(expr2.Body);

        return Expression.Lambda<Func<T, bool>>(
            Expression.AndAlso(left, right), parameter);
    }



    private class ReplaceExpressionVisitor
        : ExpressionVisitor
    {
        private readonly Expression _oldValue;
        private readonly Expression _newValue;

        public ReplaceExpressionVisitor(Expression oldValue, Expression newValue)
        {
            _oldValue = oldValue;
            _newValue = newValue;
        }

        public override Expression Visit(Expression node)
        {
            if (node == _oldValue)
                return _newValue;
            return base.Visit(node);
        }
    }
Marc Gravell
sumber
Hei Marc, saya mencoba saran pertama Anda, di blok kode pertama Anda di atas, tetapi ketika saya meneruskan ekspresi "lambda" <func <T, bool >> menghasilkan metode Where, saya mendapatkan pesan kesalahan mengatakan parameternya adalah keluar dari ruang lingkup? ada ide? cheers
andy
1
+1 versi umum berfungsi seperti mantra, saya menggunakan Dan bukannya andalso, saya pikir LINQ ke sql tidak mendukung andalso?
Maslow
2
@Maslow - inilah penulis ulang yang dapat menyejajarkan pohon untuk menyimpan Invoke: stackoverflow.com/questions/1717444/...
Marc Gravell
1
@Ron sekarang melihat tanggal: pengunjung .NET framework ( ExpressionVisitor) tidak ada saat itu; Saya punya contoh terkait pada stackoverflow dari tanggal yang sama di mana ia mengimplementasikan pengunjung secara manual: banyak kode.
Marc Gravell
1
@MarkGravell, saya menggunakan solusi pertama Anda untuk menggabungkan ekspresi saya, dan semuanya bekerja dengan baik bahkan di entitasframework, Jadi apa manfaat menggunakan solusi terakhir?
johnny 5
62

Anda dapat menggunakan Expression.AndAlso / OrElse untuk menggabungkan ekspresi logis, tetapi Anda harus memastikan ParameterExpressions adalah sama.

Saya mengalami masalah dengan EF dan PredicateBuilder sehingga saya membuatnya sendiri tanpa menggunakan Invoke, yang dapat saya gunakan seperti ini:

var filterC = filterA.And(filterb);

Kode sumber untuk PredicateBuilder saya:

public static class PredicateBuilder {

    public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> a, Expression<Func<T, bool>> b) {    

        ParameterExpression p = a.Parameters[0];

        SubstExpressionVisitor visitor = new SubstExpressionVisitor();
        visitor.subst[b.Parameters[0]] = p;

        Expression body = Expression.AndAlso(a.Body, visitor.Visit(b.Body));
        return Expression.Lambda<Func<T, bool>>(body, p);
    }

    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> a, Expression<Func<T, bool>> b) {    

        ParameterExpression p = a.Parameters[0];

        SubstExpressionVisitor visitor = new SubstExpressionVisitor();
        visitor.subst[b.Parameters[0]] = p;

        Expression body = Expression.OrElse(a.Body, visitor.Visit(b.Body));
        return Expression.Lambda<Func<T, bool>>(body, p);
    }   
}

Dan kelas utilitas untuk mengganti parameter dalam lambda:

internal class SubstExpressionVisitor : System.Linq.Expressions.ExpressionVisitor {
        public Dictionary<Expression, Expression> subst = new Dictionary<Expression, Expression>();

        protected override Expression VisitParameter(ParameterExpression node) {
            Expression newValue;
            if (subst.TryGetValue(node, out newValue)) {
                return newValue;
            }
            return node;
        }
    }
Adam Tegen
sumber
Solusi ini adalah satu-satunya yang memungkinkan saya untuk memiliki x => x.Property == Nilai dikombinasikan dengan arg => arg.Property2 == Nilai. Alat peraga utama, sedikit pendek dan membingungkan tetapi berhasil jadi saya tidak akan mengeluh. Kudos Adam :-)
VulgarBinary
Ini solusi hebat.
Aaron Stainback
Adam, ini memecahkan masalah yang sangat mengganggu yang saya alami menggunakan penyedia Linq model SharePoint Client Object - terima kasih telah mempostingnya.
Christopher McAtackney
Ini berhasil untuk saya! Saya telah mencari berbagai solusi serta predikat pembangun dan tidak ada yang berhasil sampai saat ini. Terima kasih!
tokyo0709
Ini adalah kode yang sangat bagus. Saya tidak dapat menemukan tempat untuk menyesuaikan kode, salin dan tempel itu saja :)
Tolga Evcimen
19

Jika penyedia Anda tidak mendukung Invoke dan Anda perlu menggabungkan dua ekspresi, Anda bisa menggunakan ExpressionVisitor untuk mengganti parameter dalam ekspresi kedua dengan parameter dalam ekspresi pertama.

class ParameterUpdateVisitor : ExpressionVisitor
{
    private ParameterExpression _oldParameter;
    private ParameterExpression _newParameter;

    public ParameterUpdateVisitor(ParameterExpression oldParameter, ParameterExpression newParameter)
    {
        _oldParameter = oldParameter;
        _newParameter = newParameter;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        if (object.ReferenceEquals(node, _oldParameter))
            return _newParameter;

        return base.VisitParameter(node);
    }
}

static Expression<Func<T, bool>> UpdateParameter<T>(
    Expression<Func<T, bool>> expr,
    ParameterExpression newParameter)
{
    var visitor = new ParameterUpdateVisitor(expr.Parameters[0], newParameter);
    var body = visitor.Visit(expr.Body);

    return Expression.Lambda<Func<T, bool>>(body, newParameter);
}

[TestMethod]
public void ExpressionText()
{
    string text = "test";

    Expression<Func<Coco, bool>> expr1 = p => p.Item1.Contains(text);
    Expression<Func<Coco, bool>> expr2 = q => q.Item2.Contains(text);
    Expression<Func<Coco, bool>> expr3 = UpdateParameter(expr2, expr1.Parameters[0]);

    var expr4 = Expression.Lambda<Func<Recording, bool>>(
        Expression.OrElse(expr1.Body, expr3.Body), expr1.Parameters[0]);

    var func = expr4.Compile();

    Assert.IsTrue(func(new Coco { Item1 = "caca", Item2 = "test pipi" }));
}
Francis
sumber
1
Ini memecahkan masalah khusus saya di mana solusi lain menghasilkan pengecualian yang sama. Terima kasih.
Shaun Wilson
1
Ini solusi hebat.
Aaron Stainback
3

Tidak ada yang baru di sini selain menikahi jawaban ini dengan jawaban ini dan sedikit refactored sehingga bahkan saya mengerti apa yang terjadi:

public static class ExpressionExtensions
{
    public static Expression<Func<T, bool>> AndAlso<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
    {
        ParameterExpression parameter1 = expr1.Parameters[0];
        var visitor = new ReplaceParameterVisitor(expr2.Parameters[0], parameter1);
        var body2WithParam1 = visitor.Visit(expr2.Body);
        return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(expr1.Body, body2WithParam1), parameter1);
    }

    private class ReplaceParameterVisitor : ExpressionVisitor
    {
        private ParameterExpression _oldParameter;
        private ParameterExpression _newParameter;

        public ReplaceParameterVisitor(ParameterExpression oldParameter, ParameterExpression newParameter)
        {
            _oldParameter = oldParameter;
            _newParameter = newParameter;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (ReferenceEquals(node, _oldParameter))
                return _newParameter;

            return base.VisitParameter(node);
        }
    }
}
Dejan
sumber
Saya mengalami kesulitan memahami konsep, dan perpaduan Anda dengan beberapa jawaban lain membantunya mengklik untuk saya. Terima kasih!
Kevin M. Lapio
2

Saya perlu mencapai hasil yang sama, tetapi menggunakan sesuatu yang lebih umum (karena jenisnya tidak diketahui). Berkat jawaban Marc, akhirnya saya menemukan apa yang saya coba capai:

    public static LambdaExpression CombineOr(Type sourceType, LambdaExpression exp, LambdaExpression newExp) 
    {
        var parameter = Expression.Parameter(sourceType);

        var leftVisitor = new ReplaceExpressionVisitor(exp.Parameters[0], parameter);
        var left = leftVisitor.Visit(exp.Body);

        var rightVisitor = new ReplaceExpressionVisitor(newExp.Parameters[0], parameter);
        var right = rightVisitor.Visit(newExp.Body);

        var delegateType = typeof(Func<,>).MakeGenericType(sourceType, typeof(bool));
        return Expression.Lambda(delegateType, Expression.Or(left, right), parameter);
    }
VorTech
sumber
1

Saya menyarankan satu peningkatan lagi ke PredicateBuilder dan ExpressionVisitorsolusi. Saya menyebutnya UnifyParametersByNamedan Anda dapat menemukannya di perpustakaan MIT saya: LinqExprHelper . Hal ini memungkinkan untuk menggabungkan ekspresi lambda arbiter. Biasanya pertanyaan diajukan tentang ekspresi predikat, tetapi ide ini meluas ke ekspresi proyeksi juga.

Kode berikut menggunakan metode ExprAdresyang menciptakan ekspresi parametrized yang rumit, menggunakan inline lambda. Ekspresi rumit ini dikodekan hanya sekali, dan kemudian digunakan kembali, berkat LinqExprHelperperpustakaan mini.

public IQueryable<UbezpExt> UbezpFull
{
    get
    {
        System.Linq.Expressions.Expression<
            Func<UBEZPIECZONY, UBEZP_ADRES, UBEZP_ADRES, UbezpExt>> expr =
            (u, parAdrM, parAdrZ) => new UbezpExt
            {
                Ub = u,
                AdrM = parAdrM,
                AdrZ = parAdrZ,
            };

        // From here an expression builder ExprAdres is called.
        var expr2 = expr
            .ReplacePar("parAdrM", ExprAdres("M").Body)
            .ReplacePar("parAdrZ", ExprAdres("Z").Body);
        return UBEZPIECZONY.Select((Expression<Func<UBEZPIECZONY, UbezpExt>>)expr2);
    }
}

Dan ini adalah kode bangunan subekspresi:

public static Expression<Func<UBEZPIECZONY, UBEZP_ADRES>> ExprAdres(string sTyp)
{
    return u => u.UBEZP_ADRES.Where(a => a.TYP_ADRESU == sTyp)
        .OrderByDescending(a => a.DATAOD).FirstOrDefault();
}

Apa yang saya coba capai adalah melakukan query parametrized tanpa perlu copy-paste dan dengan kemampuan untuk menggunakan lambda inline, yang sangat cantik. Tanpa semua hal pembantu-ekspresi ini, saya akan dipaksa untuk membuat seluruh permintaan dalam sekali jalan.

Jarekczek
sumber
-7

Saya pikir ini berfungsi dengan baik, bukan?

Func<T, bool> expr1 = (x => x.Att1 == "a");
Func<T, bool> expr2 = (x => x.Att2 == "b");
Func<T, bool> expr1ANDexpr2 = (x => expr1(x) && expr2(x));
Func<T, bool> expr1ORexpr2 = (x => expr1(x) || expr2(x));
Func<T, bool> NOTexpr1 = (x => !expr1(x));
Céline
sumber
1
ini tidak dapat digunakan dalam Linq ke SQL misalnya
Romain Vergnory