1

Issue: while filtering records within a date range & matching a CityID using LINQ, the query succeeds when written in 2 steps; however, it fails when combined as one query!

How can the LINQ query be rewritten so that -- it can perform both filters (i.e. match the CityId & retrieve records in the date range in the same step to improve performance?

I got it to work in two steps fine,

i.e. do a

var Step1 = db.weekRecord.Where(x => x.CityId == CityRecord.Id).ToList();

and then

Step1.Where(x => x.date.Date >= fromDate.Date 
                 && x.date.Date <= toDate.Date)
     .ToList();

it fails when I combine them!!

  // works when done in 2 steps!! 
  var weeklyWeather = db.weekRecord
    .Where(x => x.CityId == CityRecord.Id 
      && (x.date >= weekStarting && x.date <= weekEnding))

  // - when combined results are NULL!??
  var weeklyWeather2 = 
    db.weekRecord(x => x.date.Date >= fromDate.Date && x.date.Date <= toDate.Date)
      .ToList();

After looking up other SO answers, I tried this TruncateTime as well... could not get it to work..

// is this correct, from SO answers, DbFunctions.TruncateTime
var testQueryRecrods = db.weekRecord
  .Where(x => x.CityId == CityRecord.Id)
  .Where(x => 
    DbFunctions.TruncateTime(x.date.Date) >= DbFunctions.TruncateTime(fromDate.Date) 
    && DbFunctions.TruncateTime(x.date.Date) <= DbFunctions.TruncateTime(toDate.Date))
  .ToList();

ERROR:

[NotSupportedException: The specified type member 'Date' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.]
System.Data.Entity.Core.Objects.ELinq.MemberAccessTranslator.TypedTranslate(ExpressionConverter parent, MemberExpression linq) +452
System.Data.Entity.Core.Objects.ELinq.TypedTranslator`1.Translate(ExpressionConverter parent, Expression linq) +49

aggie
  • 798
  • 2
  • 8
  • 23
  • @AkshayBheda i dont have the tool, & not sure how that would help. – aggie Dec 17 '17 at 04:18
  • 1
    You have two combined queries, clearly none of them is equivalent to the first case. Could you please tidy up your example a bit? Also you speak about null, but the problem seems to be an exception. The question is confused. – Antonín Lejsek Dec 17 '17 at 04:19
  • Your first example is only making one trip to the DB because of the `ToList`, but it will return more results than you need. Can you include what `weekStarting` and `weekEnding` are set to and what the data looks like and the actual and desired results. – juharr Dec 17 '17 at 04:21
  • Basically EF cannot translate `.Date` to sql. The reason your first query works is because you added `.ToList()` which is now stored into the memory. From there .NET framework takes over. – penleychan Dec 17 '17 at 04:54
  • Use the SQL Server profiler to see what SQL query is actually being executed when you combine the two clauses. – lesscode Dec 17 '17 at 04:55
  • @lesscode he can't, it doesn't get that far. No db called. – penleychan Dec 17 '17 at 04:56
  • Have a look at this: https://stackoverflow.com/questions/11597373/the-specified-type-member-date-is-not-supported-in-linq-to-entities-exception – penleychan Dec 17 '17 at 05:00
  • Oh, I thought he said he got null when combined (assuming he meant no results). Edit the question to make this clearer. – lesscode Dec 17 '17 at 05:01
  • @12seconds is correct, I cant see the SQL profiler, never gets there... also... at home I have sql express :D – aggie Dec 17 '17 at 06:47

3 Answers3

2

The question is confused, but I would assume the problem is the .Date. Unlike linq2sql, entity framework can not translate .Date to sql. But you can rewrite it like

var fromDateDate = fromDate.Date;
var toDateDate = toDate.Date;

var testQueryRecrods = db.weekRecord
                .Where(x => x.CityId == CityRecord.Id)
                .Where(x => DbFunctions.TruncateTime(x.date) >= fromDateDate
                         && DbFunctions.TruncateTime(x.date) <= toDateDate)
                .ToList();

And it would work. To some point. What EF generates is actually totally stupid in this case. Unlike linq2sql, EF generates query, that is not sargable (in my case*). It can run thousands of times slower than necessary. I would recommend to avoid the conversion to date completely:

var fromDateDate = fromDate.Date;
var toDateDate1 = toDate.Date.AddDays(1);

var testQueryRecrods = db.weekRecord
                .Where(x => x.CityId == CityRecord.Id)
                .Where(x => x.date >= fromDateDate 
                         && x.date < toDateDate1)
                .ToList(); 

As @juharr pointed out, when you split the query, you run first half against server and the second half as linq to objects. In that case the .Date works, but you download many more records in the first half than you need.

*the datetime type may be the problem, maybe it would work better with datetime2, I did not test this scenario

Antonín Lejsek
  • 6,003
  • 2
  • 16
  • 18
  • 2
    Just to expand why `TruncateTime` can run slow, what it does is converting to a `string`, then parsing it back to a `DateTime`. Second approach is the way to go. – penleychan Dec 17 '17 at 05:14
  • Hi, I tried both approach without using DBfunctions, but it gives me the same error in both cases... `[NotSupportedException: The specified type member 'Date' is not supported in LINQ to Entities. Only initializers, entity members, and entity na.... ` I have tried them both. My question was seeking an answer to doing both steps/filters first match ID, and then recs within the week dates in the same _step_ as *Faster query without all the conversion overhead* – aggie Dec 17 '17 at 06:04
  • @mjwills I found this out when I was troubleshooting performance issues with my application through SQL profiler. Might be documented somewhere. – penleychan Dec 17 '17 at 06:06
  • @mjwills thats the code I am using, I pasted directly from my class. When I add `tolist()` to `.Where(x => x.CityId == CityRecord.Id)` in Antonin's code it runs fine!! – aggie Dec 17 '17 at 06:45
  • @mjwills the second case/block is not throwing an error... thats a lot progress :D – aggie Dec 17 '17 at 07:04
0

An offbest suggestion is to write your own LINQ extensions for this..

public static class ext
{
    //This extension compares one date to another... if you can call from Linq
    public static bool GreaterThan(this DateTime self, DateTime CompareDate
    {
    if (self.Year > CompareDate.Year) return true;

    else if ((self.Year == CompareDate.Year) && (self.Month > CompareDate.Month)
      return true;

    else if ((self.Year == CompareDate.Year) && (self.Month ==  CompareDate.Month)  && (self.Day > CompareDate.Day))
      return true;

    return false;
    }

}
Transformer
  • 6,963
  • 2
  • 26
  • 52
  • 1
    Did you try this? I suspect this won't work with EF. – mjwills Dec 17 '17 at 10:38
  • @mjwills currently its written as a general Linq Extension, _to make it work as an Linq to Entities extension, it needs to be re-wrapped_ inside `public static IQueryable MyFilter(this IQueryable query) { //compare the date fragements return query; }` hope this helps – Transformer Dec 18 '17 at 00:38
  • When you tried that, did it work @transformer ? _I am 99.99% it **won't** work, since EF / DB doesn't know how to run it - it is basically the exact problem the OP already has._ – mjwills Dec 18 '17 at 00:55
0

I think, there is no option except using DbFunctions.TruncateTime for Linq to Entities. Because, as SQL Server query Linq to Entities should perform convertion datetime to date and the best method which can be used is DbFunctions.TruncateTime. I just debugged the DbFunctions.TruncateTime convertion and the translated query seems like;

WHERE (convert (datetime2, convert(varchar(255), [Extent1].[CreationDate], 102) ,  102)) > @p__linq__0

As you see, while performing the conversation, there is a redundant string conversation here. However, the EF would convert the datetime to date in SQL just like this 'cast(CreationDate as date)'. But it is not.

So, there are two options here.

1- If you have very huge table which the performance is affected by redundant string conversations, you should build your query manually in SQL as stored procedure or something like and execute it from context.

2- If you don't have performance considerations like that; just use DbFunctions.TruncateTime(x.date)

var testQueryRecrods = db.weekRecord
    .Where(x => x.CityId == CityRecord.Id)
    .Where(x => DbFunctions.TruncateTime(x.date) >= fromDate.Date && DbFunctions.TruncateTime(x.date) <= toDate.Date)
    .ToList();
lucky
  • 12,734
  • 4
  • 24
  • 46
  • you maybe right... i think the error is in using the extra .date inside the EF query... when I perform that outside, it does *not* throw an exception. – aggie Dec 17 '17 at 07:15
  • If the datetime to date conversation isn't must in queryable form, I suggest you to avoid conversations. Just query with original date type. – lucky Dec 17 '17 at 07:27
  • Using `TruncateTime` will almost certainly perform worse than using the `toDateDate1` technique shown in the other answer due to worse sargability (i.e. it is unlikely to use indexes). – mjwills Dec 17 '17 at 11:38