-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversion of DateTime.Date doesn't work as expected #28380
Comments
My first guess here would be some timezone-related mismatch, because of mixing local and UTC timestamps; the use of ToLocalTime above seems like it could be problematic given you're saying you're reading and writing as UTC. To be able to help further, can you share a fully runnable, minimal example showing the problem? |
Hi @roji thanks for the response. Here is a link to the Repo containing a runnable minimal example: https://github.com/elfico/dateconversion |
@elfico Running your code with a few more
Notice that the result of |
The issue raised is that the sql parameter contains the time. |
@stevendarby That doesn't repro in the full project posted:
|
@ajcvickers yeah it doesn't seem a good repro of the initial post, but try changing the setting of If you're in a non-UTC aligned time zone you might see a time in the parameter. In the UK, I see |
@stevendarby This is interesting. I suspect there is some interaction between the DateTime.Kind and the parameters, hopefully only in the way they are displayed in logging. It's worth investigating. |
Apologies @ajcvickers and @stevendarby , the original post was pulled from an API project and I had to create a console app to contain just the part needed. |
@ajcvickers It doesn't seem to just be the logging, I used SQL Server Profiler and captured
Commenting out the |
@stevendarby can you post a minimal code sample which shows this odd behavior? I'm no longer sure from the above how to minimally repro it. |
@roji Sure, see below. Can comment out the using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
{
using var context = new MyContext();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var dateTime = new DateTime(2022, 07, 07, 12, 0, 0);
var query = context.Entities.Where(x => x.DateTime.Date == dateTime.Date);
Console.WriteLine(query.ToQueryString());
}
public class MyContext : DbContext
{
public DbSet<Entity> Entities { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer("Server=.;Database=DateTest;Trusted_Connection=True;TrustServerCertificate=True;")
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Entity>()
.Property(x => x.DateTime)
.HasConversion(
v => v.ToUniversalTime(),
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
}
}
public class Entity
{
public int Id { get; set; }
public DateTime DateTime { get; set; }
} |
This seems like the expected behavior to me. Here's what's going on:
I understand it may look weird, but things are working as expected... To clarify, imagine that instead of dateTime.Date you happen to pass in a regular Unspecified dateTime instance which happens to be at midnight; you'd expect that to undergo a timezone conversion, and no longer be at midnight, right? The above is pretty much the same: we don't see the fact that .Date is being called client-side, just as if you extracted the Date call out of the LINQ query: var dateTime = new DateTime(2022, 07, 07, 12, 0, 0);
var date = dateTime.Date;
var query = context.Entities.Where(x => x.DateTime.Date == date); My general recommendation would be to work with explicitly-UTC DateTimes, rather than converting them in an EF value converter; but I'm guessing that's not something you want to do. When SqlClient adds support for DateOnly (dotnet/SqlClient#1009), that would be a good way to work around this. |
@roji I think it makes sense that a parameter values passes through a column-mapped property's converter if it the parameter is being directly compared to the property - otherwise, comparisons between different types wouldn't work (the fact this is a DateTime-DateTime converter somewhat obscures this I think). However, the parameter isn't actually being compared directly to the property, but a property on that property ( Compare to this, where instead of var time = new TimeSpan(12, 0, 0);
var query = context.Set<Entity>().Where(x => x.DateTime.TimeOfDay == time); In this case, DECLARE @__time_0 time = '12:00:00';
SELECT [e].[Id], [e].[DateTime]
FROM [Entity] AS [e]
WHERE CONVERT(time, [e].[DateTime]) = @__time_0 So I'm think that just because In summary my questions would be // Passes dateTime through Entity.DateTime converter - OK because direct comparison
var dateTime = new DateTime(2022, 07, 07, 12, 0, 0);
var query = context.Set<Entity>().Where(x => x.DateTime == dateTime);
// Does not pass time through the converter - OK because it's obvious this wouldn't make sense
var time = new TimeSpan(12, 0, 0);
query = context.Set<Entity>().Where(x => x.DateTime.TimeOfDay == time);
// Passes dateTime through Entity.DateTime convert converter - Why?
dateTime = new DateTime(2022, 07, 07, 12, 0, 0);
query = context.Set<Entity>().Where(x => x.DateTime.Date == dateTime); |
Well, consider dateTime.AddDays() instead of Date: var query = context.Entities.Where(x => x.DateTime.AddDays(1) == dateTime.AddDays(1)); It's essentially the same as the Date example above, but I suspect you do want the value converter to apply to the results of AddDays(), no?
We could indeed do this, and stop propagating the operand's type mapping when Date is applied (we could even make it return var query = context.Entities.Where(x => x.DateTime.Date == dateTime); If dateTime is Unspecified/Local, I think user's may legitimately expect it to be converted to UTC before being compared; that's why they set up the value converter, after all. In other words, I can see pros and cons in both directions, and I think some people would be unhappy with either one. If that's the case, it's probably better to not break anything and leave the current behavior. |
I agree. I enjoyed poking at this but it's a complex area and current behaviour is mostly fine. I also agree that SqlClient (and EF) support for DateOnly could help with this specific case in future. For example, one day this might be a better way to do a comparison of the date only, which the value converter probably wouldn't 'interfere' with:
|
Yep, exactly. At the end of the day, DateTime really is a badly designed type - I keep harping on how NodaTime is a superior alternative, and is even supported on some EF providers. DateOnly/TimeOnly make the situation a bit better, though I'd still be very wary of DateTime. |
@elfico is there anything that still needs addressing here from your perspective? |
Thanks @roji . I quite understand you explanation. I was thinking that since .Date was called on the datetime object then a conversion will also be applied to the supplied input in the sql query just like the conversion applied to the database column. What would you suggest as the best way to go about this. Removing the value converter would mean refactoring the entire codebase to convert all dates to UTC first before saving. We figured doing it in one place would eliminate that kind of work and also ensure future code converts automatically rather than relying on the engineer to remember to convert to UTC. Thanks for you timely response. |
First, while I understand the desire to avoid refactoring your entire codebase to use UTC, I do recommend considering this. If the intent is to store UTC timestamps, then it's highly recommended that your DateTime instances be of Kind=UTC. This would also prevent possible bugs if someone calls e.g. ToUniversalTime, or various other cases. Bottom line: if it's UTC, mark it as such. That aside, you should be able to specifically work around this issue by making sure that the specific DateTime instance is UTC: // Assume we get an Unspecified DateTime from somewhere:
var dateTime = new DateTime(2022, 07, 07, 12, 0, 0);
// Mark the Unspecified DateTime as UTC as follows (this can be done outside the query as well).
// This will make the result of Date be a (truncated) UTC DateTime as well, and your value converter calling ToUniversalTime won't have any effect on it.
var query = context.Entities.Where(x => x.DateTime.Date == DateTime.SpecifyKind(dateTime, DateTimeKind.Utc).Date); |
Thanks @roji for the recommendations. I will discuss with this with my team. Thanks @stevendarby and @ajcvickers for your contributions. |
Hello everyone,
We have a project we are working on and we need to query the database for some data for a particular date.
We configured our DB to read and write date as UTC.
When writing the query to get the data, I noticed that the data for a date was not being pulled from the database.
Here is the code:
On investigation, I noticed that when pulling the data, the DB returns the date as UTC as it should and the date is compared to the input date. But no data is returned. I checked the query generated and noticed this:
From the above, the query generated shows that the
TransactionDate
is converted to justDate
and compared to the input date@__transactionDate_1
which is inDateTime
form.Any help on this will be deeply appreciated.
The text was updated successfully, but these errors were encountered: