Генерация параметра для Linq для обработки диапазонов в датах - PullRequest
0 голосов
/ 07 декабря 2018

Я хочу получить следующее выражение или как можно ближе к нему.

IRangePredicate range = new Range(); 

DbContext context = new dbModel();

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

и range создаст частичное выражение для запроса linq, которое может быть разрешено как:

CreatedAt >= from && CreatedAt <= to 

Или

CreatedAt >= from

Или

CreatedAt <= To

для использования в запросе linq.

В конце концов, я хотел бы расширить этот метод, включив в него также возможности «меньше или больше» без равных.

и использовать его как своего рода «внедрение зависимости аргумента».Однако мои попытки даже не компилируются, поскольку Expression<Func<DateTime, bool>> нельзя использовать как частичный параметр, и мне нужно определить следующий запрос для этих специальных фильтров.Что я не хочу делать.Я хочу, чтобы он читался как «обычный» Linq.

Или мне нужно просто вставить их как Func.Это может сработать, но как только я попытаюсь сделать это в Context Linq Query, эта вещь взорвется, потому что 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" для выполнения чистого поиска по ссылке в общих коллекциях.

Однако я хочу повторно использовать эту логику, или настолько близко, насколько это возможно, чтобы позволить мне выполнить это в отношении DbContext.

У меня есть базовый контроллер crud для любого типа таблицы в направленииDb, однако, я хотел бы улучшить этот бит, но позволить программисту реализовать шаблон стратегии для частичных классов, сгенерированных либо из кода сначала, либо из db первых моделей в C #.

Однако, чтобы перевести Linq в SQL, мне нужно преобразовать мой тип возврата "просто bool" в выражения.Я получил это далеко.Но как, черт возьми, я делаю «подмножества» предикатов, которые могут быть объединены в одной коллекции?Я вижу несколько примеров кода, которые требуют от вас цепочки запросов.И это может в конечном итоге стать решением.Это только кажется таким ... ужасным.

Я просто не могу заставить свой мозг придумать синтаксис для этого.И это расстраивает меня: D Прости меня, если этого нельзя сделать, потому что я просто глупый.Мне просто кажется, что это возможно.

1 Ответ

0 голосов
/ 07 декабря 2018

Вот пример реализации.Он использует метод расширения для изменения Expression s для создания нового 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 # и выигралне выводите через выражения возврата, вы должны указать тип при использовании стандарта Where, но замена IQueryable extension 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();

И затем вызывать его с предварительно созданным FilterDataRange иFilterStartsWith:

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