2

I want to achieve the following expression or as near as possible.

IRangePredicate range = new Range(); 

DbContext context = new dbModel();

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

and range will produce a partial expression for the linq query, that can either be resolved as:

CreatedAt >= from && CreatedAt <= to 

Or

CreatedAt >= from

Or

CreatedAt <= To

to be used in the linq query.

Eventually, I would like to extend this method to include the possibilities of less or more without equals as well.

and use it as a sort of "arguement dependency injection". However, my attempts fail to even compile, as either Expression<Func<DateTime, bool>> can't be used as a partial parameter, and I need to define the following query for these special filters. Which I don't want to do. I want it to read as "normal" Linq.

Or I need to simply insert them as Func only. Which might work, but as soon as I try to do that on a Context Linq Query, the thing explodes, because Entity Framework, does not play well if it is not formatted as an Expression

Can anyone guide me in the right direction?

Example of what I tried: (Please note this does not compile, because that is my entire issue :D )

EDIT From here: -I have commented out the line of code that doesn't compile, so you have a compilable example. It just doesn't work if you try to do it on a DbContext set.

 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);
        }
    }

Okay, So I added the base method that game me the idea, as a demo example, where I am just using ordinary "bool" to accomplish a pure search by link in generic collections.

However, I want to reuse this logic, or as close as possibe, to allow me to accomplish this towards a DbContext.

I have a base crud controller for any type of table towards the Db, however, I would like to enhance this bit, but letting the programmer to implement a strategy pattern over a partial classes generated from either code first, or db first models in C#.

However, in order to translate the Linq to SQL, I need to convert my "just bool" return type into expressions. I got that far. But How the heck do I make "subsets" of predicates, that can be unified over a single collection? I see some code examples that require you to chain the queries. And that might end up being the solution. This just seems so... ugly.

I just can't get my brain to think up the syntax to do this. And it is frustrating me :D Forgive me if this cannot be done, because I am simply stupid. It just seems sort of intuitive to me that this should be possible.

Morten Bork
  • 1,413
  • 11
  • 23
  • Not sure, but I think your expression should be `Expression>` – TGnat Dec 07 '18 at 14:56
  • I am stuck at: ```list.Where(r.GetRange).Where(x=>x.typeId == 1).ToList();``` with ```public Expression> GetRange(Invoice param) { return (x => param.CreatedAt >= _from && param.CreatedAt <= _to); }``` which doesn't work,yet, but maybe its a step further than your are(), also this could be interesting: https://stackoverflow.com/a/19618035/1037841 for you – FrankM Dec 07 '18 at 15:11
  • @FrankM It defeats the entire purpose of having a range class if it can only be used with the `Invoice` class. At that point you're better off just not having the class at all and writing the whole thing out by hand every time (which is what's trying to be avoided by pulling the logic into a more generic class). – Servy Dec 07 '18 at 15:16
  • One of @Servy's old answers about almost exactly this: https://stackoverflow.com/a/24962550/1202807. Although maybe things have changed to make this a bit easier since then. – Gabriel Luci Dec 07 '18 at 15:20
  • @GabrielLuci You'll then need to use another compose (or [pull a predicate builder out of one of my other answers](https://stackoverflow.com/a/22407189/1159478)) to do the rest of the condition though too. – Servy Dec 07 '18 at 15:22
  • @Servy so you think its not possible or just very hard/complicated to achieve the ops goal? – FrankM Dec 07 '18 at 15:23
  • @FrankM Part of that depends on what you consider "good enough". Do you consider the solution of mine that Gabriel linked a suitably simple solution (when considering you still need to combine the rest of the predicate, as I mentioned)? If so, then it's not *that* hard. – Servy Dec 07 '18 at 15:25
  • I tried a little further and found out, that ```List<>.Where()``` differs from ```IQueryable.Where()```, the later does support expressions, so in the [Testcase] from above List must be replaced by an implementation of IQueryable. not sure which to use – FrankM Dec 07 '18 at 15:45
  • You can do `.Where(range.InRange(x => x.member))` where `InRange` returns an `Expression>` but C# doesn't compose or nest `Expression`s automatically. You could also use [LINQKit](https://github.com/scottksmith95/LINQKit) or something similar to help create and compose. Given the complications, I agree that a `WhereInRange(range)` extension method on `IQueryable` makes more sense. – NetMage Dec 07 '18 at 17:57
  • @NetMage I have made an edit that shows how I got to this point, I am not sure this really helps, but it at least explains why I thought this would be possible :) Now I don't mind creating something that does generic handling in the background, perhaps even something that does some sort of decorator pattern approach to whole thing. But this sort of thinking feels instinctively ... messy. Is there no way to combine expression for Linq in C#? At this point I am just worried I am stupid. – Morten Bork Dec 07 '18 at 19:02
  • @MortenBork You *can* do it. It's not nearly as easy as it is for regular functions. See the links to other answers of mine already linked in comments for examples of ways to do it. – Servy Dec 07 '18 at 19:29
  • @Servy I see where your previous responses are going. However, it doesn't really work with my example. I want to be able to replace my Range Class with a "RangeFrom" for example, that only has a from date. Yet, it still implements the original "Expression> method. (I now see why people want to say "Invoice" instead of "DateTime" And the syntax is also wrong there. However, that is my mistake. Likely an Interface to provide a created date instead would be better so "Expression would be better. Allowing any class using that interface to be used – Morten Bork Dec 07 '18 at 19:51
  • @Servy and the whole point is I want to do an easy overload of part of the query. If I have to define the expression seperately, especially the ones that type specific, to meld with those who just want the "CreatedAt" datetime field from the db object, then this method doesn't really apply. – Morten Bork Dec 07 '18 at 20:08

1 Answers1

1

Here is a sample implementation. It uses an extension method for modifying Expressions to build a new 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);
}

Now you can build a filter interface and some implementations to represent differing types of filters and an IQueryable extension that uses it to filter:

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));
}

Given all this, you use it like so:

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);

Since the C# type inference engine isn't as sophisticated as e.g. F# and won't infer through return expressions, you must specify the type when using the standard Where but an IQueryable extension replacement Where can infer the type from the first parameter (e.g. Accounts).

Since the IFilter is generic, you can use other types of filters such as FilterStartsWith to filter on other types of fields:

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

And then call it with a pre-created FilterDataRange and FilterStartsWith:

var nameFilter = new FilterStartsWith("TEST");
var ans = Table1InRangeWithName(yesterdayFilter, nameFilter);
NetMage
  • 26,163
  • 3
  • 34
  • 55
  • I get what you are doing here. But this doesn't really fix the sprit of the issue. I want it to be more like a strategy pattern. Where I can overload a parameter "part" by providing the parameter via an interface of sorts. Meaning, I can redefine, for example how a linq query will execute a range search, by passing a method a new instance of for example an interface IGetRange. Thus eliminating the need to test and write the same range definitions for every single f***ing table in the database. (Which I way I gave you points, this will work, but its not the answer I am looking for) – Morten Bork Dec 08 '18 at 08:24
  • @MortenBork `RangeFilter` should work for any table, because it is generic. You have to create an instance of `RangeFilter` for a specific table because you have to have the proper type to access the members of the table for the tests, but you could make the test creation generic and `RangeFilter` a non-generic class instead...? – NetMage Dec 11 '18 at 23:05
  • I apologize for my lack of vocabulary. I will figure out a way to explain myself better, because it is my lack of ability, to explain myself that is causing confusion. I will try to find some time, very soon, to demonstrate what I mean, by "not the solution" in spirit. – Morten Bork Dec 12 '18 at 07:56
  • @MortenBork I added an example at the bottom of creating a method that combines a range with other criteria, taking the range as a parameter. Is that what you mean? Or do you have more types of filters you desire than just the date range? – NetMage Dec 12 '18 at 20:46
  • @MortenBork I replaced the code to show how you can create a generic filter range interface, and then build two different `DateTime` filters and use them. You could also make the interface even more generic and not involving ranges. – NetMage Dec 12 '18 at 21:04
  • @MortenBork Re-written again to make the filter interface generic in member type and demonstrate multiple filter classes. – NetMage Dec 12 '18 at 21:19
  • I appreciate the effort Netmage. If I could award you more points. I would :D Your solution is much better than what I had even after the tips provided. I still think there is a little room for improvement. But what goes on under the hood, is basically what your response is. So I will mark it as "Answered". However, I will respond with my own code once I get it to work as I want, so you can see what I ended up doing with your otherwise excellent answer :) – Morten Bork Dec 13 '18 at 10:02
  • @MortenBork I look forward to it! – NetMage Dec 13 '18 at 18:56