Introduction
I'm using EF Core with .NET 5.0 and SQL Server Express. Basically I'm wondering if it generated a buggy SQL query or if my code is buggy (probably :D). I provided a mre at the bottom of the question, but hopefully the problem becomes evident from the data I've collected (I've already asked a similar question, but felt that it needed a complete overhaul)
Setup
I have a record and a DbContext
like the following. It's stripped down to the important property Moment
, which must be of type DateTimeOffset
(company guideline).
private class Foo
{
public int ID { get; set; }
public DateTimeOffset Moment { get; set; }
}
private class Context : DbContext
{
public Context(DbContextOptions<Context> options) : base(options) {}
public DbSet<Foo> Foos { get; set; }
}
The respective column in the generated database has a datatype of datetimeoffset(7)
, seems fine.
I initialized the database with this data (5 consecutive days, each at midnight in the respective time zone):
context.Foos.Add(new Foo() { Moment = DateTimeOffset.Parse("2021-04-21 00:00 +02:00"), });
context.Foos.Add(new Foo() { Moment = DateTimeOffset.Parse("2021-04-22 00:00 +02:00"), });
context.Foos.Add(new Foo() { Moment = DateTimeOffset.Parse("2021-04-23 00:00 +02:00"), });
context.Foos.Add(new Foo() { Moment = DateTimeOffset.Parse("2021-04-24 00:00 +02:00"), });
context.Foos.Add(new Foo() { Moment = DateTimeOffset.Parse("2021-04-25 00:00 +02:00"), });
Now I want to query all records Moment >= start && Moment <= end
while ignoring the time with these parameters:
var start = DateTimeOffset.Parse("2021-04-22 00:00 +02:00");
var end = DateTimeOffset.Parse("2021-04-24 00:00 +02:00");
I expect to get 3 records and came up with 3 queries that seem identical to me, however, the second yields a different result:
Queries
private static async Task Query1(Context context, DateTimeOffset start, DateTimeOffset end)
{
var records = await context.Foos
.Where(foo => foo.Moment.Date >= start.Date && foo.Moment.Date <= end.Date)
//... Finds 3 records, expected
}
private static async Task Query2(Context context, DateTimeOffset start, DateTimeOffset end)
{
start = start.Date; // .Date yields DateTime -> implicit conversion to DateTimeOffset?
end = end.Date;
var records = await context.Foos
.Where(foo => foo.Moment.Date >= start && foo.Moment.Date <= end)
// ... Finds only 2 records, unexpected
}
private static async Task Query3(Context context, DateTimeOffset start, DateTimeOffset end)
{
var start2 = start.Date; // start2 and end2 are of type DateTime now
var end2 = end.Date;
var records = await context.Foos
.Where(foo => foo.Moment.Date >= start2 && foo.Moment.Date <= end2)
// ... Finds 3 records, expected
}
Results
I also made a LINQ version of each query where I queried data from a List<Foo>
:
private static void Query1(List<Foo> foos, DateTimeOffset start, DateTimeOffset end)
{
var records = foos
.Where(foo => foo.Moment.Date >= start.Date && foo.Moment.Date <= end.Date)
//...
}
Function | Expected number of records | Records from DB | Records when using LINQ |
---|---|---|---|
Query1 | 3 | 3 | 3 |
Query2 | 3 | 2 | 3 |
Query3 | 3 | 3 | 3 |
Why does the second database query only return 2 records?
I know that the first and third query compare DateTime
with DateTime
while the second one compares DateTime
with DateTimeOffset
. Thus I'm wondering why the LINQ-versions behave differently.
Traced Queries
I traced the actual queries that were send to the SQL Server and they are different, however, I don't really understand why they lead to different results (not much SQL experience):
-- From Query1()
exec sp_executesql N'SELECT [f].[ID]
FROM [Foos] AS [f]
WHERE (CONVERT(date, [f].[Moment]) >= @__start_Date_0) AND (CONVERT(date, [f].[Moment]) <= @__end_Date_1)',N'@__start_Date_0 datetime2(7),@__end_Date_1 datetime2(7)',@__start_Date_0='2021-04-22 00:00:00',@__end_Date_1='2021-04-24 00:00:00'
-- From Query2()
exec sp_executesql N'SELECT [f].[ID]
FROM [Foos] AS [f]
WHERE (CAST(CONVERT(date, [f].[Moment]) AS datetimeoffset) >= @__start_0) AND (CAST(CONVERT(date, [f].[Moment]) AS datetimeoffset) <= @__end_1)',N'@__start_0 datetimeoffset(7),@__end_1 datetimeoffset(7)',@__start_0='2021-04-22 00:00:00 +02:00',@__end_1='2021-04-24 00:00:00 +02:00'
-- From Query3()
exec sp_executesql N'SELECT [f].[ID]
FROM [Foos] AS [f]
WHERE (CONVERT(date, [f].[Moment]) >= @__start2_0) AND (CONVERT(date, [f].[Moment]) <= @__end2_1)',N'@__start2_0 datetime2(7),@__end2_1 datetime2(7)',@__start2_0='2021-04-22 00:00:00',@__end2_1='2021-04-24 00:00:00'
MRE
Tested with Microsoft.EntityFrameworkCore
and Microsoft.EntityFrameworkCore.SqlServer
Version5.0.6
.
namespace Playground
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
public static class Program
{
private class Foo
{
public int ID { get; set; }
public DateTimeOffset Moment { get; set; }
}
private static Context CreateContext()
{
var connectionString = $"Data Source=.\\SQLEXPRESS;Initial Catalog=FOO_DB;Integrated Security=SSPI";
var optionsBuilder = new DbContextOptionsBuilder<Context>();
optionsBuilder.UseSqlServer(connectionString).EnableSensitiveDataLogging();
var context = new Context(optionsBuilder.Options);
context.Database.EnsureCreated();
return context;
}
private class Context : DbContext
{
public Context(DbContextOptions<Context> options) : base(options) { }
public DbSet<Foo> Foos { get; set; }
}
private static async Task Query1(Context context, DateTimeOffset start, DateTimeOffset end)
{
var records = await context.Foos
.Where(foo => foo.Moment.Date >= start.Date && foo.Moment.Date <= end.Date)
.Select(foo => foo.ID)
.ToListAsync();
Console.WriteLine($"Query1 in DB found {records.Count} records");
}
private static async Task Query2(Context context, DateTimeOffset start, DateTimeOffset end)
{
start = start.Date;
end = end.Date;
var records = await context.Foos
.Where(foo => foo.Moment.Date >= start && foo.Moment.Date <= end)
.Select(foo => foo.ID)
.ToListAsync();
Console.WriteLine($"Query2 in DB found {records.Count} records");
}
private static async Task Query3(Context context, DateTimeOffset start, DateTimeOffset end)
{
var start2 = start.Date;
var end2 = end.Date;
var records = await context.Foos
.Where(foo => foo.Moment.Date >= start2 && foo.Moment.Date <= end2)
.Select(foo => foo.ID)
.ToListAsync();
Console.WriteLine($"Query3 in DB found {records.Count} records");
}
public async static Task Main()
{
var context = CreateContext();
var foos = new List<Foo>() {
new Foo() { Moment = DateTimeOffset.Parse("2021-04-21 00:00 +02:00"), },
new Foo() { Moment = DateTimeOffset.Parse("2021-04-22 00:00 +02:00"), },
new Foo() { Moment = DateTimeOffset.Parse("2021-04-23 00:00 +02:00"), },
new Foo() { Moment = DateTimeOffset.Parse("2021-04-24 00:00 +02:00"), },
new Foo() { Moment = DateTimeOffset.Parse("2021-04-25 00:00 +02:00"), },
};
if (!context.Foos.Any())
{
await context.AddRangeAsync(foos);
}
await context.SaveChangesAsync();
var start = DateTimeOffset.Parse("2021-04-22 00:00 +02:00");
var end = DateTimeOffset.Parse("2021-04-24 00:00 +02:00");
await Query1(context, start, end);
await Query2(context, start, end);
await Query3(context, start, end);
context.Dispose();
}
}
}