在 Entity Framework 查询中使用 MemberExpression。 Entity Framework 带有 MemberExpression 的 IQueryable 扩展

Use MemberExpression in Entity Framework query. Entity Framework IQueryable extension with MemberExpression

主线任务

为 Entity Framework 创建 IQueryable 扩展,其中可以使用 MemberExpression 设置查询中使用的字段。

WhereOverlap 是示例。 WhereOverlap 通过两个 DateTime 字段构建对实体的查询。
Orders 有 DateTime 字段 StartDate、EndDate。
Trips 有 DateTime 字段 From、To。

var dateFrom = DateTime.Now.AddDays(-30);
var dateTo = DateTime.Now.AddDays(-15);

var orders = await db.Orders
    .WhereOverlap(
        // MemberExpressions (Orders has StartDate, EndDate fields)
        fromField: es => es.StartDate,
        toField: es => es.EndDate,

        from: dateFrom,
        to: dateTo)
    .ToListAsync();

var trips = await db.Trips
    .WhereOverlap(
        // MemberExpressions (Trips has From, To fields)
        fromField: es => es.From,
        toField: es => es.To,

        from: dateFrom,
        to: dateTo)
    .ToListAsync();

要实现我们想要的,我们需要回答以下 2 个问题中的任何一个:

或问题 1

要创建此类 IQueryable 扩展,需要创建具有以下签名的 Where 扩展:

public static class QueryableWhereOverlapExtensions
{
    static IQueryable<TEnt> Where<TEnt, TParam, TParam2>(this IQueryable<TEnt> source,
        Expression<Func<TEnt, TParam>> property,
        Expression<Func<TEnt, TParam2>> property2,
        Expression<Func<TParam, TParam2, bool>> where)
    {
        // ??? HOW TO DO THIS
        return ...
    }

    public static IQueryable<TEnt> WhereOverlap<TEnt>(this IQueryable<TEnt> source,
        Expression<Func<TEnt, DateTime>> startField,
        Expression<Func<TEnt, DateTime>> endField,
        DateTime from, DateTime to)
    {
        // example of usage Where extension
        return source.Where(startField, endField,
            (start, end) => start <= to && end >= from);
    }
}

如何实现带有显示签名的 IQueryable Where 扩展?

或问题 2

可以使用 A universal PredicateBuilder 构建动态查询形式的表达式 (Expression>)。 所以如果我们有这样的方法,我们将能够创建 WhereOverlap 扩展

static Expression<Func<TEnt, bool>> ApplayWhere<TEnt, TParam>(this Expression<Func<TEnt, TParam>> field, 
    Expression<Func<TParam, bool>> where) 
{
    // ??? HOW TO DO THIS
}

public static IQueryable<TEnt> WhereOverlap<TEnt>(this IQueryable<TEnt> source,
    Expression<Func<TEnt, DateTime>> startField,
    Expression<Func<TEnt, DateTime>> endField,
    DateTime from, DateTime to)
{
    // example of usage ApplayWhere
    Expression<Func<TEnt, bool>> startExpr = startField.ApplayWhere(start => start <= to);
    Expression<Func<TEnt, bool>> endExpr = endField.ApplayWhere(end => end >= from);

    return source.Where(PredicateBuilder.And(startExpr, endExpr));
}

如何实现AppplayWhere?

这不完全是一个答案。但是我创建了 WhereOverlap 扩展,并提供了一种创建可重用 linq 查询的方法,可以使用 MemberExpression 来设置查询中使用的字段。

首先我们需要 MemberExpressionExtensions:

    public static class MemberExpressionExtensions {
        public static Expression<Func<TEnt, bool>> HasVal<TEnt, TProp>(this Expression<Func<TEnt, TProp?>> field) where TProp : struct
            => Expression.Lambda<Func<TEnt, bool>>(Expression.NotEqual(field.Body, Expression.Constant(null, typeof(TProp?))), field.Parameters);

        public static Expression<Func<TEnt, bool>> HasNoVal<TEnt, TProp>(this Expression<Func<TEnt, TProp?>> field) where TProp : struct
            => Expression.Lambda<Func<TEnt, bool>>(Expression.Equal(field.Body, Expression.Constant(null, typeof(TProp?))), field.Parameters);

        public static Expression<Func<TEnt, bool>> LessOrEqual<TEnt, TProp>(this Expression<Func<TEnt, TProp>> field, TProp val)
            => Expression.Lambda<Func<TEnt, bool>>(Expression.LessThanOrEqual(field.Body, Expression.Constant(val, typeof(TProp))), field.Parameters);

        public static Expression<Func<TEnt, bool>> Less<TEnt, TProp>(this Expression<Func<TEnt, TProp>> field, TProp val)
            => Expression.Lambda<Func<TEnt, bool>>(Expression.LessThan(field.Body, Expression.Constant(val, typeof(TProp))), field.Parameters);

        public static Expression<Func<TEnt, bool>> GreaterOrEqual<TEnt, TProp>(this Expression<Func<TEnt, TProp>> field, TProp val)
            => Expression.Lambda<Func<TEnt, bool>>(Expression.GreaterThanOrEqual(field.Body, Expression.Constant(val, typeof(TProp))), field.Parameters);

        public static Expression<Func<TEnt, bool>> Greater<TEnt, TProp>(this Expression<Func<TEnt, TProp>> field, TProp val)
            => Expression.Lambda<Func<TEnt, bool>>(Expression.GreaterThan(field.Body, Expression.Constant(val, typeof(TProp))), field.Parameters);
    }

现在我们可以创建可重复使用的查询构建器

class BigPayFilter {
    readonly decimal Limit;
    public BigPayFilter(decimal limit) {
        Limit = limit;
    }

    public Expression<Func<TEnt, bool>> Create<TEnt>(
        Expression<Func<TEnt, decimal>> field) {

        // GreaterOrEqual is extension
        return field.GreaterOrEqual(Limit);
    }
}

用法。 例如,有 Payout 和 Premium 对象:

class Payout { 
    public decimal Total { get; set; }
}

class Premium {
    public decimal Sum { get; set; }
}
// filter to find payments greater or equal 1000
//
// you can get limit value from configuration (in this example limit is 1000),
// and put BigPayFilter to IoC-container
var bigPayFilter = new BigPayFilter(1000);


// use BigPayFilter for payouts

var payoutPredicate =
    bigPayFilter.Create<Payout>(pp => pp.Total);

var payouts = new[] {
    new Payout{ Total = 100 },
    new Payout{ Total = 50 },
    new Payout{ Total = 25.5m },
    new Payout{ Total = 1050.67m }
}
.AsQueryable()
.Where(payoutPredicate)
.ToList();


// use BigPayFilter for premiums

var premiumPredicate =
    bigPayFilter.Create<Premium>(pp => pp.Sum);

var premiums = new[] {
    new Premium{ Sum = 2000 },
    new Premium{ Sum = 50.08m },
    new Premium{ Sum = 25.5m },
    new Premium{ Sum = 1070.07m }
}
.AsQueryable()
.Where(premiumPredicate)
.ToList();

对于更复杂的查询,您必须将 MemberExpressionExtensions 与由 Pete Montgomery 创建的“A universal PredicateBuilder”结合使用。

此解决方案完全支持 Entity Framework,包括异步操作。

我用这个解决方案创建了NuGet and GitHub