为 Linq 生成一个参数来处理日期范围

Generating a parameter for Linq to handle ranges in dates

我想实现或尽可能接近下面的表达式。

IRangePredicate range = new Range(); 

DbContext context = new dbModel();

context.Table1.Where(x => range.IsInRange(x.CreatedAt) && x.type == 1).ToList();

and range 将为 linq 查询生成一个部分表达式,可以解析为:

CreatedAt >= from && CreatedAt <= to 

CreatedAt >= from

CreatedAt <= To

用于 linq 查询。

最后,我想扩展此方法以包括 less 或 more 没有 equals 的可能性。

并将其用作 "arguement dependency injection" 的一种。 但是,我的尝试甚至无法编译,因为 Expression<Func<DateTime, bool>> 不能用作部分参数,我需要为这些特殊过滤器定义以下查询。我不想这样做。我希望它读作 "normal" Linq。

或者我只需将它们作为 Func 插入即可。这可能行得通,但是一旦我尝试在上下文 Linq 查询上执行此操作,事情就会爆炸,因为 Entity Framework,如果未格式化为表达式

,则效果不佳

谁能指导我正确的方向?

我尝试过的示例:(请注意这不会编译,因为这是我的全部问题 :D )

从这里编辑: -我已经注释掉了不能编译的代码行,所以你有一个可编译的例子。如果您尝试在 DbContext 集上执行它,它就不起作用。

 public interface IRangeFunctional
    {
        bool GetRange(DateTime param);
    }

    public interface IRange
    {
        Expression<Func<DateTime, bool>> GetRange(DateTime param);
    }

    public class RangeFunctional : IRangeFunctional
    {
        private DateTime _from;
        private DateTime _to;
        public RangeFunctional(DateTime from, DateTime to)
        {
            _from = from;
            _to = to;
        }

        public bool GetRange(DateTime param)
        {
            return param >= _from && param <= _to;
        }
    }

    public class Range : IRange
    {
        private DateTime _from;
        private DateTime _to;
        public Range(DateTime from, DateTime to)
        {
            _from = from;
            _to = to;
        }

        public Expression<Func<DateTime, bool>> GetRange(DateTime param)
        {
            return (x => param >= _from && param <= _to);
        }
    }

    public class Invoice
    {
        public DateTime CreatedAt { get; set; }
        public int typeId { get; set; }
    }

    [TestClass]
    public class TestRange
    {
        List<Invoice> list = new List<Invoice>()
        {
            new Invoice()
            {
                CreatedAt = new DateTime(2018,1,1,0,0,0), typeId = 1
            },
            new Invoice()
            {
                CreatedAt = new DateTime(2018,1,2,0,0,0), typeId = 1
            },
            new Invoice()
            {
                CreatedAt = new DateTime(2018,1,1,0,0,0), typeId = 2
            },
            new Invoice()
            {
                CreatedAt = new DateTime(2018,1,2,0,0,0), typeId = 2
            }
        };

        [TestMethod]
        public void RangeTest()
        {
            Range r = new Range(new DateTime(2018, 1, 1, 0, 0, 0), new DateTime(2018, 1, 2, 0, 0, 0));
            RangeFunctional rf = new RangeFunctional(new DateTime(2018, 1, 1, 0, 0, 0), new DateTime(2018, 1, 2, 0, 0, 0));
            List<Invoice> partialListFunc = list.Where(x => x.typeId == 2 && rf.GetRange(x.CreatedAt)).ToList();


            //List<Invoice> partialList = list.Where(x => x.typeId == 2 && r.GetRange(x.CreatedAt)).ToList();
            Assert.AreEqual(2, partialListFunc.Count);
        }
    }

好的,所以我添加了让我产生想法的基本方法,作为演示示例,我只是使用普通的 "bool" 在通用集合中通过 link 完成纯搜索。

但是,我想重用这个逻辑,或者尽可能接近,以允许我在 DbContext 中完成这个。

我有一个针对 Db 的任何类型 table 的基本 crud 控制器,但是,我想增强这一点,但让程序员在部分 类 从代码优先或 C# 中的数据库优先模型生成。

但是,为了将 Linq 转换为 SQL,我需要将我的 "just bool" return 类型转换为表达式。我做到了那么远。但是我到底如何制作 "subsets" 谓词,可以在单个集合上统一? 我看到一些代码示例需要您链接查询。这可能最终成为解决方案。这看起来太……丑陋了。

我只是无法动脑筋想出执行此操作的语法。这让我很沮丧 :D 如果不能做到这一点,请原谅我,因为我只是愚蠢。这应该是可能的,这对我来说似乎有点直觉。

这是一个示例实现。它使用扩展方法修改 Expressions 来构建一个新的 Expression:

public static class ExpressionExt {
    /// <summary>
    /// Replaces a sub-Expression with another Expression inside an Expression
    /// </summary>
    /// <param name="orig">The original Expression.</param>
    /// <param name="from">The from Expression.</param>
    /// <param name="to">The to Expression.</param>
    /// <returns>Expression with all occurrences of from replaced with to</returns>
    public static Expression Replace(this Expression orig, Expression from, Expression to) => new ReplaceVisitor(from, to).Visit(orig);
}

/// <summary>
/// Standard ExpressionVisitor to replace an Expression with another in an Expression.
/// </summary>
public class ReplaceVisitor : ExpressionVisitor {
    readonly Expression from;
    readonly Expression to;

    public ReplaceVisitor(Expression from, Expression to) {
        this.from = from;
        this.to = to;
    }

    public override Expression Visit(Expression node) => node == from ? to : base.Visit(node);
}

现在您可以构建一个过滤器接口和一些实现来表示不同类型的过滤器,以及一个 IQueryable 使用它来过滤的扩展:

public interface IFilter<TMember> {
    Expression<Func<TData, bool>> FilterFn<TData>(Expression<Func<TData, TMember>> memberFn);
}

public class FilterDateTimeRange : IFilter<DateTime?> {
    public DateTime? from;
    public DateTime? to;

    public FilterDateTimeRange(DateTime? fromDT, DateTime? toDT) {
        from = fromDT;
        to = toDT;
    }

    public Expression<Func<T, bool>> FilterFn<T>(Expression<Func<T, DateTime?>> memberFn) {
        Expression<Func<DateTime?, bool>> rangeBodyTemplate;
        if (from.HasValue) {
            if (to.HasValue)
                rangeBodyTemplate = dt => from.Value <= dt && dt <= to.Value;
            else
                rangeBodyTemplate = dt => from.Value <= dt;
        }
        else if (to.HasValue) {
            rangeBodyTemplate = dt => dt <= to.Value;
        }
        else
            rangeBodyTemplate = dt => true;

        return Expression.Lambda<Func<T, bool>>(rangeBodyTemplate.Body.Replace(rangeBodyTemplate.Parameters[0], memberFn.Body), memberFn.Parameters);
    }
}

public class FilterDateRange : IFilter<DateTime?> {
    public DateTime? from;
    public DateTime? to;

    public FilterDateRange(DateTime? fromDT, DateTime? toDT) {
        from = fromDT?.Date;
        to = toDT?.Date;
    }

    public Expression<Func<T, bool>> FilterFn<T>(Expression<Func<T, DateTime?>> memberFn) {
        Expression<Func<DateTime?, bool>> rangeBodyTemplate;
        if (from.HasValue) {
            if (to.HasValue)
                rangeBodyTemplate = dt => from <= (dt == null ? dt : dt.Value.Date) && (dt == null ? dt : dt.Value.Date) <= to;
            else
                rangeBodyTemplate = dt => from.Value <= (dt == null ? dt : dt.Value.Date);
        }
        else if (to.HasValue) {
            rangeBodyTemplate = dt => (dt == null ? dt : dt.Value.Date) <= to.Value;
        }
        else
            rangeBodyTemplate = dt => true;

        return Expression.Lambda<Func<T, bool>>(rangeBodyTemplate.Body.Replace(rangeBodyTemplate.Parameters[0], memberFn.Body), memberFn.Parameters);
    }
}

public class FilterStartsWith : IFilter<String> {
    public string start;

    public FilterStartsWith(string start) => this.start = start;

    public Expression<Func<T, bool>> FilterFn<T>(Expression<Func<T, string>> memberFn) {
        Expression<Func<string, bool>> rangeBodyTemplate;
        if (!String.IsNullOrEmpty(start))
            rangeBodyTemplate = s => s.StartsWith(start);
        else
            rangeBodyTemplate = s => true;

        return Expression.Lambda<Func<T, bool>>(rangeBodyTemplate.Body.Replace(rangeBodyTemplate.Parameters[0], memberFn.Body), memberFn.Parameters);
    }
}

public static class FilterExt {
    public static IQueryable<TData> WhereFilteredBy<TData, TMember>(this IQueryable<TData> src, IFilter<TMember> r, Expression<Func<TData, TMember>> memberFn) => src.Where(r.FilterFn(memberFn));
}

考虑到所有这些,您可以像这样使用它:

var r1 = new FilterDateTimeRange(DateTime.Now.AddDays(-1).Date, DateTime.Now.AddDays(-1).Date);
var yesterdayFilter = new FilterDateRange(DateTime.Now.AddDays(-1), DateTime.Now.AddDays(-1));

var r1a = Accounts.Where(r1.RangeFilter<Accounts>(a => a.Modified_date));
var ya = Accounts.WhereFilteredBy(yesterdayFilter, a => a.Modified_date);

因为 C# 类型推理引擎不像例如F# 并且不会通过 return 表达式推断,使用标准 Where 时必须指定类型,但是 IQueryable 扩展替换 Where 可以从第一个参数推断类型(例如 Accounts)。

由于 IFilter 是通用的,您可以使用其他类型的过滤器,例如 FilterStartsWith 来过滤其他类型的字段:

List<Table1> Table1InRangeWithName(IFilter<DateTime?> range, IFilter<string> name) => context.Table1.WhereFilteredBy(range, t1 => t1.Modified_date).WhereFilteredBy(name, t1 => t1.Name).ToList();

然后用预先创建的FilterDataRangeFilterStartsWith调用它:

var nameFilter = new FilterStartsWith("TEST");
var ans = Table1InRangeWithName(yesterdayFilter, nameFilter);