根據外部傳入參數,動態組裝資料庫查詢條件,這是一個很常見的需求,多數的程式設計師應該都有實作過,這天我在使用 Entity Framework Core 做動態條件組裝的時候,「OR
」條件讓我卡了一下。
使用情境
簡單介紹一下我的使用情境,我有 date1
跟 date2
兩個日期參數,使用者指定了 date1,就找出日期等於 date1 的資料,使用者指定了 date2,就找出日期等於 date2 的資料,date1 跟 date2 條件是獨立的,也就是說,使用者同時指定了 date1 及 date2,那麼這兩個日期的資料都要有,很典型的「OR」條件。
public async Task<List<MyData>> GetData(DateTime? date1, DateTime? date2)
{
var dbCtx = new MyDatabaseContext(...);
var query = dbCtx.MyData.Where(x => false);
if (date1.HasValue)
{
// OR date1 ...
}
if (date2.HasValue)
{
// OR date2 ...
}
var dataList = await query.ToListAsync();
return dataList;
}
可是我就在 // OR date1 ...
跟 // OR date2 ...
卡住了,經過一番研究,我得到兩個解法。
解法一
第一種解法,是直接在 Where
方法中撰寫 OR 條件。
public async Task<List<MyData>> GetData(DateTime? date1, DateTime? date2)
{
var dbCtx = new MyDatabaseContext(...);
var query = dbCtx.MyData.Where(x => (date1.HasValue && x.Date == date1) || (date2.HasValue && x.Date == date2));
var dataList = await query.ToListAsync();
return dataList;
}
這種解法沒問題,但是條件參數一多的時候,Where 裡面的條件陳述式就變得很長。
解法二
第二種解法是我參考了一個套件 - LINQKit,我沒有安裝它,它裡面功能很多,我是看了裡面 PredicateBuilder.cs 的原始碼,另外弄出動態組裝條件的類別。
public static class PredicateBuilder
{
private class RebindParameterVisitor(ParameterExpression oldParameter, ParameterExpression newParameter) : ExpressionVisitor
{
protected override Expression VisitParameter(ParameterExpression node)
{
if (node == oldParameter)
{
return newParameter;
}
return base.VisitParameter(node);
}
}
public static Expression<Func<T, bool>> New<T>(IEnumerable<T> enumerable, Expression<Func<T, bool>> startExpr)
{
return startExpr;
}
public static Expression<Func<T, bool>> NewPredicate<T>(this IEnumerable<T> enumerable, Expression<Func<T, bool>> startExpr)
{
return New(enumerable, startExpr);
}
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{
var right = new RebindParameterVisitor(expr2.Parameters[0], expr1.Parameters[0]).Visit(expr2.Body);
return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(expr1.Body, right), expr1.Parameters);
}
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{
var right = new RebindParameterVisitor(expr2.Parameters[0], expr1.Parameters[0]).Visit(expr2.Body);
return Expression.Lambda<Func<T, bool>>(Expression.OrElse(expr1.Body, right), expr1.Parameters);
}
public 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);
}
}
有了這個類別的相助,原本全部摻在 Where 方法裡面的條件陳述式就可以拆出來了。
public async Task<List<MyData>> GetData(DateTime? date1, DateTime? date2)
{
var dbCtx = new MyDatabaseContext(...);
var predicate = dbCtx.MyData.NewPredicate(x => false)
if (date1.HasValue)
{
predicate = predicate.Or(x => x.Date == date1);
}
if (date2.HasValue)
{
predicate = predicate.Or(x => x.Date == date2);
}
var dataList = await dbCtx.MyData.Where(predicate).ToListAsync();
return dataList;
}
我選擇第二種解法,因為我不想看到 Where 方法裡面塞一堆字,如果大家有更好的解法,歡迎留言告訴我。