dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.65k stars 3.15k forks source link

DateTime.Kind comes back as Unspecified #4711

Closed gzak closed 1 year ago

gzak commented 8 years ago

If you store a DateTime object to the DB with a DateTimeKind of either Utc or Local, when you read that record back from the DB you'll get a DateTime object whose kind is Unspecified. Basically, it appears the Kind property isn't persisted. This means that a test to compare equality of the before/after value of a round trip to the DB will fail.

Is there something that needs to be configured for the Kind to be preserved? Or what's the recommended way to handle this?

gzak commented 8 years ago
var entity = db.Entity.Single(e => e.Id == 123);
var now = DateTime.UtcNow;
entity.Date = now;
db.SaveChanges();

entity = db.Entity.Single(e => e.Id == 123);
Assert.Equal(now, entity.Date); // fails the assert
rmja commented 8 years ago

A slight variation of this is that it would be nice if one could configure the default DateTimeKind. For example, I know that all dates in my database are Utc, so it would be nice if this was specified for all fetched dates in the DbContext, or maybe configurable per entity property?

ilmax commented 8 years ago

+1 on whar @rmja suggested, it's a really nice addition.

gzak commented 8 years ago

One thing worth noting is that I think the DateTime type in Cā™Æ has more precision than the DB equivalent, so equality probably won't be preserved after a round trip even with this change.

ajcvickers commented 8 years ago

Note for triage: Investigated mapping to DateTime CLR type to SQL Server datetimeoffset by default, but even when switching to the typed value buffer factory this still won't work without type conversions.

rowanmiller commented 8 years ago

Conclusions from triage:

rmja commented 8 years ago

@rowanmiller how about my suggestion? It would be really useful in systems where the kind does not need to be stored, because it is agreed upon by convention.

rowanmiller commented 8 years ago

@rmja I should have mentioned that too... at the moment we rely on ADO.NET to construct the value and then we set it on the property. We do want to introduce a feature where you can control how that value is get/set from/to the property. This would allow things like setting DateTime.Kind, type conversions, using Get/Set methods rather than properties, etc. https://github.com/aspnet/EntityFramework/issues/240 is the issue tracking this feature

gzak commented 8 years ago

@rowanmiller would you mind giving an example (like my second comment) that uses DateTimeOffset to correct the issue? Also, what type should I use for the corresponding column in the DB (assuming MSSQL server)?

gzak commented 8 years ago

Never mind my last comment, I didn't realize there was a corresponding sql type called datetimeoffset. Basically, we've switched everything over to the -offset variants in both C# and the DB which solved this issue as well as #5175.

rowanmiller commented 8 years ago

@gzak glad it's working for you now

hikalkan commented 7 years ago

I also would want such a convention @rmja mentioned (https://github.com/aspnet/EntityFramework/issues/4711#issuecomment-201405020). Because not all database providers have datetimeoffset type.

hikalkan commented 7 years ago

I found a solution for that and shared as a blog post: http://volosoft.com/datetime-specifykind-in-entity-framework-core-while-querying/

havheg commented 7 years ago

I had this same issue today. Why is Datetime/Timezones still an issue? I don't really care how DateTime is stored in the database as long as the returned value is the same as when I stored it. It could even be stored as a standardized(ISO 8601) string for all I care.

EEaglehouse commented 6 years ago

Switching to DateTimeOffset doesn't solve the problem. It isn't time zone aware; it only stores the UTC offset for a specific instant in time. There is no simple way to derive the time zone where the value was created. Picking the underlying UTC value out of the DateTimeOffset value would be the best you could do and DateTime is just as good at assuming that.

ajcvickers commented 6 years ago

@EEaglehouse and others. I'm working on a change that will allow the kind to be be preserved by storing the DateTime as a long in the database and automatically converting it on the way in and out. Should be in 2.1 as part of #242.

EEaglehouse commented 6 years ago

@ajcvickers Thank you. It would be nice to have that as an option. For me, the better solution remains to have an option to force the Kind to a particular value (Utc in my case) for DateTime values. Reading it as a DateTimeOffset is useless to me because I would still need to convert to DateTime with Kind as Utc in my applications' data layer; all local time zone conversions are handled in the presentation layer. Converting DateTime to a long for storage may be useful, but would effectively obfuscate the column type in the database. But please don't take this as discouraging you from implementing the custom type mapping, because then I could do for myself what I'm asking.

ajcvickers commented 6 years ago

@EEaglehouse Yes, after #242 you will be able to set things up to still store as a datetime in the database, but force a specific kind whenever it is read from the database.

werwolfby commented 6 years ago

We are using this updated workaround from: http://volosoft.com/datetime-specifykind-in-entity-framework-core-while-querying/ Thank you @hikalkan, but I've made few changes, after few hours of debugging EF:

    public class DateTimeKindMapper
    {
        public static DateTime Normalize(DateTime value)
            => DateTime.SpecifyKind(value, DateTimeKind.Utc);

        public static DateTime? NormalizeNullable(DateTime? value)
            => value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : (DateTime?)null;

        public static object NormalizeObject(object value)
            => value is DateTime dateTime ? Normalize(dateTime) : value;
    }

    public class DateTimeKindEntityMaterializerSource : EntityMaterializerSource
    {
        private static readonly MethodInfo _normalizeMethod =
            typeof(DateTimeKindMapper).GetTypeInfo().GetMethod(nameof(DateTimeKindMapper.Normalize));

        private static readonly MethodInfo _normalizeNullableMethod =
            typeof(DateTimeKindMapper).GetTypeInfo().GetMethod(nameof(DateTimeKindMapper.NormalizeNullable));

        private static readonly MethodInfo _normalizeObjectMethod =
            typeof(DateTimeKindMapper).GetTypeInfo().GetMethod(nameof(DateTimeKindMapper.NormalizeObject));

        public override Expression CreateReadValueExpression(Expression valueBuffer, Type type, int index, IProperty property = null)
        {
            if (type == typeof(DateTime))
            {
                return Expression.Call(
                    _normalizeMethod,
                    base.CreateReadValueExpression(valueBuffer, type, index, property));
            }

            if (type == typeof(DateTime?))
            {
                return Expression.Call(
                    _normalizeNullableMethod,
                    base.CreateReadValueExpression(valueBuffer, type, index, property));
            }

            return base.CreateReadValueExpression(valueBuffer, type, index, property);
        }

        public override Expression CreateReadValueCallExpression(Expression valueBuffer, int index)
        {
            var readValueCallExpression = base.CreateReadValueCallExpression(valueBuffer, index);
            if (readValueCallExpression.Type == typeof(DateTime))
            {
                return Expression.Call(
                    _normalizeMethod,
                    readValueCallExpression);
            }

            if (readValueCallExpression.Type == typeof(DateTime?))
            {
                return Expression.Call(
                    _normalizeNullableMethod,
                    readValueCallExpression);
            }

            if (readValueCallExpression.Type == typeof(object))
            {
                return Expression.Call(
                    _normalizeObjectMethod,
                    readValueCallExpression);
            }

            return readValueCallExpression;
        }
    }

Workaround from @hikalkan works, except cases in subquery, like:

context.Entity.Select(e => new
{
    DateTimeField = e.NavigationProperty.OrderBy(n => n.OrderField).FirstOrDefault().DateTimeField
});

So extended EntityMaterializerSource can handle this as well, but because it wraps every object response as well, it can cause additional performance cost because of object to DateTime cast.

And it has additional fix for nullable DateTime as well. Before 2.1 we will use this workaround.

EEaglehouse commented 6 years ago

Hi, Alexander,

That looks like a nice solution and more advanced than the solution we have been using, which was to use T4 to generate a custom DBContext. The code you presented is pretty easy to understand and not very complicated. When we migrate to .NET Core, this will definitely come in handy.

Thank you very much for taking the time to share it with me!

--Ed

From: Alexander Puzynia [mailto:notifications@github.com] Sent: Thursday, January 18, 2018 1:32 AM To: aspnet/EntityFrameworkCore EntityFrameworkCore@noreply.github.com Cc: Ed Eaglehouse eeaglehouse@buckeyemountain.com; Mention mention@noreply.github.com Subject: Re: [aspnet/EntityFrameworkCore] DateTime.Kind comes back as Unspecified (#4711)

We are using this updated workaround from: http://volosoft.com/datetime-specifykind-in-entity-framework-core-while-querying/

public class DateTimeKindMapper

{

    public static DateTime Normalize(DateTime value)

        => DateTime.SpecifyKind(value, DateTimeKind.Utc);

    public static DateTime? NormalizeNullable(DateTime? value)

        => value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : (DateTime?)null;

    public static object NormalizeObject(object value)

        => value is DateTime dateTime ? Normalize(dateTime) : value;

}

public class DateTimeKindEntityMaterializerSource : EntityMaterializerSource

{

    private static readonly MethodInfo _normalizeMethod =

        typeof(DateTimeKindMapper).GetTypeInfo().GetMethod(nameof(DateTimeKindMapper.Normalize));

    private static readonly MethodInfo _normalizeNullableMethod =

        typeof(DateTimeKindMapper).GetTypeInfo().GetMethod(nameof(DateTimeKindMapper.NormalizeNullable));

    private static readonly MethodInfo _normalizeObjectMethod =

        typeof(DateTimeKindMapper).GetTypeInfo().GetMethod(nameof(DateTimeKindMapper.NormalizeObject));

    public override Expression CreateReadValueExpression(Expression valueBuffer, Type type, int index, IProperty property = null)

    {

        if (type == typeof(DateTime))

        {

            return Expression.Call(

                _normalizeMethod,

                base.CreateReadValueExpression(valueBuffer, type, index, property));

        }

        if (type == typeof(DateTime?))

        {

            return Expression.Call(

                _normalizeNullableMethod,

                base.CreateReadValueExpression(valueBuffer, type, index, property));

        }

        return base.CreateReadValueExpression(valueBuffer, type, index, property);

    }

    public override Expression CreateReadValueCallExpression(Expression valueBuffer, int index)

    {

        var readValueCallExpression = base.CreateReadValueCallExpression(valueBuffer, index);

        if (readValueCallExpression.Type == typeof(DateTime))

        {

            return Expression.Call(

                _normalizeMethod,

                readValueCallExpression);

        }

        if (readValueCallExpression.Type == typeof(DateTime?))

        {

            return Expression.Call(

                _normalizeNullableMethod,

                readValueCallExpression);

        }

        if (readValueCallExpression.Type == typeof(object))

        {

            return Expression.Call(

                _normalizeObjectMethod,

                readValueCallExpression);

        }

        return readValueCallExpression;

    }

}

Workaround from volosoft works, except cases in subquery, like:

context.Entity.Select(e => new

{

DateTimeField = e.NavigationProperty.OrderBy(n => n.OrderField).FirstOrDefault().DateTimeField

});

So extended EntityMaterializerSource can handle this as well, but because it wraps every object response as well, it can cause additional performance cost because of object to DateTime cast.

And it has additional fix for nullable DateTime as well. Before 2.1 we will use this workaround.

ā€” You are receiving this because you were mentioned. Reply to this email directly, view it on GitHubhttps://github.com/aspnet/EntityFrameworkCore/issues/4711#issuecomment-358551156, or mute the threadhttps://github.com/notifications/unsubscribe-auth/ASrwvT9VrPRI9gyXit0b6RQOJd0K7-zyks5tLuVMgaJpZM4HrCRe.

ajcvickers commented 6 years ago

Please note that EntityMaterializerSource is internal code and any solution like that shown above may get broken with a new release of EF. That doesn't mean I'm saying not to use it--it's publicly available internal code for a reason--but please be aware of the risk associated with it.

Starting in EF Core 2.1, this would be one way to deal with DateTime.Kind:

modelBuilder
    .Entity<Foo>()
    .Property(e => e.SomeDate)
    .HasConversion(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Local));

This will ensure every time the date is read from the database it is specified as Local automatically.

werwolfby commented 6 years ago

@ajcvickers but my snippet above works with projections as well.

And we are waiting for EF Core 2.1 to remove this code from our solution, but until 2.1 we have to use it šŸ˜ž.

ajcvickers commented 6 years ago

@werwolfby You are correct--I copy pasted that code snippet from another post and missed that last sentence, which was relevant there but not here. The workaround you posted is a good one--this is probably the best way to make this work, and is, in effect, what the value converter in 2.1 is doing.

Being able to do things like this is why we made the internal code available. However, we have to be careful that internal code is only used when people understand the risks. Otherwise too many people may start complaining when they get broken and we will be under pressure to make it really internal again.

werwolfby commented 6 years ago

@ajcvickers Yes I understand the risk using this code. And I completely agree, but we have to use it, and thank you to make this API public available šŸ˜„. And I create gist to keep this "helper" up-to-date until EF Core 2.1: https://gist.github.com/werwolfby/7f04558bc21c8114e209d5727fb2e9f8

cycbluesky commented 6 years ago

Mark DateTimeKind

martavoi commented 6 years ago

@ajcvickers .HasConversion option don't cover computation, i.e:

var res = context.Select(i => new {Deadline = i.Created + i.Estimated});
werwolfby commented 6 years ago

@martavoi This is expected, because you're selecting anonymous object, and you didn't (and can't) specify conversion for anonymous type. And even if Created and Estimated has conversion, this will not help because it was converted to Sql without any conversion.

And as far as I understand you can't solve this issue with current .HasConversion functionality. Because you can specify it only for Entities, but not for anonymous type.

There should be something to map to views, maybe it will help.

martavoi commented 6 years ago

@werwolfby i would say there must be smth allow us to configure DateTime materialization for entire Context. We usually store UTC dates and expect to see DateTime.Kind == UTC for all queries

werwolfby commented 6 years ago

@martavoi, we had tried to migrate to EF Core 2.1 as well. And yep, only few queries starts to work, but all this projection still doesn't work. This is because all this Value Converter can be set for properties only, it doesn't affect projection. So we still continue to use my workaround from above šŸ˜ž (gist)

We still need something from EF, to be able work on projections as well. CCed @ajcvickers.

For example this projection works partially, but I expected that it should works always:

context.Entities.Select(e => new ProjectionEntity
{
    LastTested = e.Results
        .Where(x => x.CompletedDate.HasValue)
        .OrderByDescending(x => x.CompletedDate)
        .FirstOrDefault().
        .CompletedDate,
    RevisionDate = e.CreationDate
}

And RevisionDate converted right using value converter, while CompletedDate don't. Both properties had converters.

I think this issue should be reopened.

ajcvickers commented 6 years ago

@martavoi Can you please post a new issue with a complete runnable project/solution or code listing that reproduces the behavior you are seeing? It would be good to infer the converter to use for Deadline, but I don't know how feasible it is now.

@werwolfby Can you also post another new issue with a complete runnable project/solution that reproduces what you are seeing? I think we need to understand better how that query is being executed in order to understand what is going on.

werwolfby commented 6 years ago

@ajcvickers Will try to implement it in few days.

But the main question here is still the same. Do you plan to allow specify converters globally on type, instead of on each property?

ajcvickers commented 6 years ago

@werwolfby This is tracked by #10784

springy76 commented 6 years ago

The EF 6.x provider of System.Data.SQLite has an option for the connection string which lets you configure which DateTimeKind ANY DateTime retrieved by the provider will have no matter if entities or anonymous objects or whatsoever. SINCE YEARS!

forlayo commented 5 years ago

I am using the solution that @rowanmiller suggest; and I would say that it should be considered as correct solution.

Because, as he points, SQL Server does not allow to store that datetime is on an specific timezone, and setting a default one break the consistency conceptually through the system.

You can do same you're doing with datetime with datetimeoffset and it would include the timezone, so why not using it?

EEaglehouse commented 5 years ago

Because datetimeoffset includes only the difference between UTC and local time with the saved datetime value; it does not include the time zone. Anyone who has to deal with different time zones is well-advised to learn the difference. The offset alone cannot identify the actual time zone nor if Daylight Saving Time was in effect, so datetimeoffset is of extremely limited use. Internally, it is represented by a UTC value anyway, so it adds little value over simply saving a datetime value as UTC in the first place. People need to be educated to not use datetimeoffset unless they know in what rare and isolated cases it may be truly helpful. Slapping datetimeoffset in as a replacement for datetime and expecting to have the time zone included is misguided, at best, because it won't do that. If HasConversion() or a ValueConverter can be applied to types rather than just properties, it would be a good solution to supplying the expected DateTimeKind.

Compufreak345 commented 5 years ago

If anyone finds it helpful - I modified the great snippet from @ajcvickers to make sure all my dates are saved as UTC and read as UTC.

foreach(var entityType in modelBuilder.Model.GetEntityTypes()) {
 foreach(var property in entityType.GetProperties()) {
  if (property.ClrType == typeof(DateTime)) {
   modelBuilder.Entity(entityType.ClrType)
    .Property < DateTime > (property.Name)
    .HasConversion(
     v => v.ToUniversalTime(),
     v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
  } else if (property.ClrType == typeof(DateTime ? )) {
   modelBuilder.Entity(entityType.ClrType)
    .Property < DateTime ? > (property.Name)
    .HasConversion(
     v => v.HasValue ? v.Value.ToUniversalTime() : v,
     v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : v);
  }
 }
}
springy76 commented 5 years ago

@Compufreak345 will this work with queries composing into anonymous (or non-entity) types?

Compufreak345 commented 5 years ago

@Compufreak345 will this work with queries composing into anonymous (or non-entity) types?

I am not sure what you mean by "queries composing into anonymous types", if you do your writing database transactions using LINQ to SQL with entities it should work for everything you do. It should also work if you convert your properties to another object type while reading LINQ entities, if you do something outside of entity framework like hard-coded SQL queries it will not.

clement-fifty commented 5 years ago

Note that in my case, I have queries (View equivalent) and that doesn't seem to like this workaround, so I had to use the following :

foreach(var entityType in builder.Model.GetEntityTypes()) {
    if (entityType.IsQueryType) continue; // This is the difference!

    foreach(var property in entityType.GetProperties()) {
        if (property.ClrType == typeof(DateTime)) {
            builder.Entity(entityType.ClrType)
                .Property < DateTime > (property.Name)
                .HasConversion(
                    v => v.ToUniversalTime(),
                    v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
        } else if (property.ClrType == typeof(DateTime ? )) {
            builder.Entity(entityType.ClrType)
                .Property < DateTime ? > (property.Name)
                .HasConversion(
                    v => v.HasValue ? v.Value.ToUniversalTime() : v,
                    v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : v);
        }
    }
}
ChristopherHaws commented 4 years ago

@clement-fifty I wasn't able to get your solution working whenever I had the property manually configured (for example if I give the column a custom name in the db). I was able to make it work by doing the following though:

var dateTimeConverter = new ValueConverter<DateTime, DateTime>(
    v => v.ToUniversalTime(),
    v => DateTime.SpecifyKind(v, DateTimeKind.Utc));

var nullableDateTimeConverter = new ValueConverter<DateTime?, DateTime?>(
    v => v.HasValue ? v.Value.ToUniversalTime() : v,
    v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : v);

foreach (var entityType in builder.Model.GetEntityTypes())
{
    if (entityType.IsQueryType)
    {
        continue;
    }

    foreach (var property in entityType.GetProperties())
    {
        if (property.ClrType == typeof(DateTime))
        {
            property.SetValueConverter(dateTimeConverter);
        }
        else if (property.ClrType == typeof(DateTime?))
        {
            property.SetValueConverter(nullableDateTimeConverter);
        }
    }
}
btecu commented 4 years ago

.IsQueryType doesn't seem to be present for EF Core 3.

smitpatel commented 4 years ago

entityType.IsKeyless

jeremycook commented 4 years ago

I also check the column type since I have some 'date' fields in my database that are not UTC. For a SQL Server datatime field that looks like this:

property.GetColumnType() == "datetime"
ChristopherHaws commented 4 years ago

@jeremycook I recently have run into a similar problem where certain fields I didnt want to be tracked as UTC, so I made the following changes to my code:

public class ApplicationContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
        builder.ApplyUtcDateTimeConverter();
    }
}

public static class UtcDateAnnotation
{
    private const string IsUtcAnnotation = "IsUtc";
    private static readonly ValueConverter<DateTime, DateTime> UtcConverter =
        new ValueConverter<DateTime, DateTime>(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc));

    public static PropertyBuilder<TProperty> IsUtc<TProperty>(this PropertyBuilder<TProperty> builder, bool isUtc = true) =>
        builder.HasAnnotation(IsUtcAnnotation, isUtc);

    public static bool IsUtc(this IMutableProperty property) =>
        ((bool?)property.FindAnnotation(IsUtcAnnotation)?.Value) ?? true;

    /// <summary>
    /// Make sure this is called after configuring all your entities.
    /// </summary>
    public static void ApplyUtcDateTimeConverter(this ModelBuilder builder)
    {
        foreach (var entityType in builder.Model.GetEntityTypes())
        {
            foreach (var property in entityType.GetProperties())
            {
                if (!property.IsUtc())
                {
                    continue;
                }

                if (property.ClrType == typeof(DateTime) ||
                    property.ClrType == typeof(DateTime?))
                {
                    property.SetValueConverter(UtcConverter);
                }
            }
        }
    }
}

Which allows me to mark a property as not UTC with the following code (UTC is true by default):

builder.Property(x => x.DateField).IsUtc(false);
JonPSmith commented 4 years ago

Can I ask a question on nullable value converters. The EF docs says

A null value will never be passed to a value converter. This makes the implementation of conversions easier and allows them to be shared amongst nullable and non-nullable properties.

(I bolded the part in question).

But The comment by @ChristopherHaws shows two versions for a DateTime and DateTime?. Is that necessary or can you assume that your converter won't be called if the value is null?

EEaglehouse commented 4 years ago

Great question. The way I read that note, if the only difference in the second ValueConverter is checking for null, it won't be called anyway. In the DateTime? case here, it would be unnecessary.

But thinking about it, it says a null value will never be passed to a value converter and this allows them to be shared. But it does not say a null value cannot be returned from a value converter (say for example, converting an empty string to a null), so a nullable option could still be useful.

Would somebody who has experience with this please comment?

ajcvickers commented 4 years ago

@JonPSmith @EEaglehouse Null will never be passed to a converter, so the same non-nullable converter can be used for the PK and the FK. We propagate the converter set on the PK automatically to FKs, so you only really need to set the converter on the PK.

Returning null from a converter is possible, but may result in unexpected behavior for FKs since it changes the relationship.

ChristopherHaws commented 4 years ago

@EEaglehouse I updated the code snippet to exclude the nullable case. Thanks @ajcvickers for confirming that. šŸ‘

gabynevada commented 4 years ago

@ChristopherHaws I modified the code snippet to include Utc conversion for nullable DateTime objects (DateTime?)

public class ApplicationContext : DbContext
{
  protected override void OnModelCreating(ModelBuilder builder)
  {
      base.OnModelCreating(builder);

      builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
      builder.ApplyUtcDateTimeConverter();
  }
}

public static class UtcDateAnnotation
{
  private const String IsUtcAnnotation = "IsUtc";
  private static readonly ValueConverter<DateTime, DateTime> UtcConverter =
    new ValueConverter<DateTime, DateTime>(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc));

  private static readonly ValueConverter<DateTime?, DateTime?> UtcNullableConverter =
    new ValueConverter<DateTime?, DateTime?>(v => v, v => v == null ? v : DateTime.SpecifyKind(v.Value, DateTimeKind.Utc));

  public static PropertyBuilder<TProperty> IsUtc<TProperty>(this PropertyBuilder<TProperty> builder, Boolean isUtc = true) =>
    builder.HasAnnotation(IsUtcAnnotation, isUtc);

  public static Boolean IsUtc(this IMutableProperty property) =>
    ((Boolean?)property.FindAnnotation(IsUtcAnnotation)?.Value) ?? true;

  /// <summary>
  /// Make sure this is called after configuring all your entities.
  /// </summary>
  public static void ApplyUtcDateTimeConverter(this ModelBuilder builder)
  {
    foreach (var entityType in builder.Model.GetEntityTypes())
    {
      foreach (var property in entityType.GetProperties())
      {
        if (!property.IsUtc())
        {
          continue;
        }

        if (property.ClrType == typeof(DateTime))
        {
          property.SetValueConverter(UtcConverter);
        }

        if (property.ClrType == typeof(DateTime?))
        {
          property.SetValueConverter(UtcNullableConverter);
        }
      }
    }
  }
}
ChristopherHaws commented 4 years ago

Null will never be passed to a converter, so the same non-nullable converter can be used for the PK and the FK. We propagate the converter set on the PK automatically to FKs, so you only really need to set the converter on the PK.

Returning null from a converter is possible, but may result in unexpected behavior for FKs since it changes the relationship.

@gabynevada Per this comment, you shouldn't need to handle null, that is why I removed it from my snippet. Converters don't get called for null values.