Efcore: Allow specifying a default DateTimeKind

Created on 14 Aug 2018  路  3Comments  路  Source: dotnet/efcore

This is related to #4711 and #10784, basically a follow-up issue based on a comment by @rmja:

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?


Usually, when you are designing a database, you have the full control over how dates might be stored inside of it. So regardless of whether your database supports storing the timeszone with the DateTime or not, it鈥檚 not uncommon to decide on a fixed timezone for all dates inside of a database. Usually, and that is also the general recommendation on this topic, this would be UTC.

It would be really nice to be able to set up a default handling for the DateTimeKind. For example, a good and flexible rule could be this: If the kind is unspecified, interpret it as a UTC DateTime; otherwise convert it into a UTC DateTime, taking its kind into account.

I鈥檓 currently doing this with a value converter like this:

```c#
Expression> normalizeDate = d => d.Kind == DateTimeKind.Unspecified ? DateTime.SpecifyKind(d, DateTimeKind.Utc) : d.ToUniversalTime();
var dateTimeNormalizer = new ValueConverter(normalizeDate, normalizeDate);

// and then for every single DateTime property on every entity
entity.Property(e => e.EverySingleDateTimeProperty).HasConversion(dateTimeNormalizer);
```

This works pretty well but having to add this on every single DateTime property is really tedious. Of course, this particular solution will become obsolete when configuring a value converter globally for a property type becomes possible with (maybe) 3.0. But regardless of that, I think it would be worth to consider adding support for date time kinds in general, that is independent of value converters.

What do you think? Am I ignoring missing use cases that would break such a default, or would implementing this separately from default value converters be too complicated?

closed-duplicate

Most helpful comment

Following other peoples suggestions (ModelBuilderExtension etc) in both #4711 and #10784, I decided to go ahead with:

public static class ModelBuilderExtension
{
    public static ModelBuilder UseValueConverterForType<T>(this ModelBuilder modelBuilder, ValueConverter converter)
    {
        return modelBuilder.UseValueConverterForType(typeof(T), converter);
    }

    public static ModelBuilder UseValueConverterForType(this ModelBuilder modelBuilder, Type type, ValueConverter converter)
    {
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            // note that entityType.GetProperties() will throw an exception, so we have to use reflection
            var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == type);

            foreach (var property in properties)
            {
                modelBuilder.Entity(entityType.Name).Property(property.Name)
                    .HasConversion(converter);
            }
        }

        return modelBuilder;
    }

    public class DateTimeKindValueConverter : ValueConverter<DateTime, DateTime>
    {
        public DateTimeKindValueConverter(DateTimeKind kind, ConverterMappingHints mappingHints = null)
            : base(
                v => v.ToUniversalTime(),
                v => DateTime.SpecifyKind(v, kind),
                mappingHints)
        {
        }
    }

    public static void SetDefaultDateTimeKind(this ModelBuilder modelBuilder, DateTimeKind kind)
    {
        modelBuilder.UseValueConverterForType<DateTime>(new DateTimeKindValueConverter(kind));
        modelBuilder.UseValueConverterForType<DateTime?>(new DateTimeKindValueConverter(kind));
    }
}

Allows you to specify the default in the DbContext:

protected override void OnModelCreating(ModelBuilder builder)
{
    builder.SetDefaultDateTimeKind(DateTimeKind.Utc);
    base.OnModelCreating(builder);
}

All 3 comments

Following other peoples suggestions (ModelBuilderExtension etc) in both #4711 and #10784, I decided to go ahead with:

public static class ModelBuilderExtension
{
    public static ModelBuilder UseValueConverterForType<T>(this ModelBuilder modelBuilder, ValueConverter converter)
    {
        return modelBuilder.UseValueConverterForType(typeof(T), converter);
    }

    public static ModelBuilder UseValueConverterForType(this ModelBuilder modelBuilder, Type type, ValueConverter converter)
    {
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            // note that entityType.GetProperties() will throw an exception, so we have to use reflection
            var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == type);

            foreach (var property in properties)
            {
                modelBuilder.Entity(entityType.Name).Property(property.Name)
                    .HasConversion(converter);
            }
        }

        return modelBuilder;
    }

    public class DateTimeKindValueConverter : ValueConverter<DateTime, DateTime>
    {
        public DateTimeKindValueConverter(DateTimeKind kind, ConverterMappingHints mappingHints = null)
            : base(
                v => v.ToUniversalTime(),
                v => DateTime.SpecifyKind(v, kind),
                mappingHints)
        {
        }
    }

    public static void SetDefaultDateTimeKind(this ModelBuilder modelBuilder, DateTimeKind kind)
    {
        modelBuilder.UseValueConverterForType<DateTime>(new DateTimeKindValueConverter(kind));
        modelBuilder.UseValueConverterForType<DateTime?>(new DateTimeKindValueConverter(kind));
    }
}

Allows you to specify the default in the DbContext:

protected override void OnModelCreating(ModelBuilder builder)
{
    builder.SetDefaultDateTimeKind(DateTimeKind.Utc);
    base.OnModelCreating(builder);
}

Duplicate of #10784?

Following other peoples suggestions (ModelBuilderExtension etc) in both #4711 and #10784, I decided to go ahead with:

public static class ModelBuilderExtension
{
  public static ModelBuilder UseValueConverterForType<T>(this ModelBuilder modelBuilder, ValueConverter converter)
  {
      return modelBuilder.UseValueConverterForType(typeof(T), converter);
  }

  public static ModelBuilder UseValueConverterForType(this ModelBuilder modelBuilder, Type type, ValueConverter converter)
  {
      foreach (var entityType in modelBuilder.Model.GetEntityTypes())
      {
          // note that entityType.GetProperties() will throw an exception, so we have to use reflection
          var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == type);

          foreach (var property in properties)
          {
              modelBuilder.Entity(entityType.Name).Property(property.Name)
                  .HasConversion(converter);
          }
      }

      return modelBuilder;
  }

  public class DateTimeKindValueConverter : ValueConverter<DateTime, DateTime>
  {
      public DateTimeKindValueConverter(DateTimeKind kind, ConverterMappingHints mappingHints = null)
          : base(
              v => v.ToUniversalTime(),
              v => DateTime.SpecifyKind(v, kind),
              mappingHints)
      {
      }
  }

  public static void SetDefaultDateTimeKind(this ModelBuilder modelBuilder, DateTimeKind kind)
  {
      modelBuilder.UseValueConverterForType<DateTime>(new DateTimeKindValueConverter(kind));
      modelBuilder.UseValueConverterForType<DateTime?>(new DateTimeKindValueConverter(kind));
  }
}

Allows you to specify the default in the DbContext:

protected override void OnModelCreating(ModelBuilder builder)
{
  builder.SetDefaultDateTimeKind(DateTimeKind.Utc);
  base.OnModelCreating(builder);
}

This solution works very well, except if you have a not mapped property on a data model.
By adding a check if a a setModel function is present this problem can be addressed too:

`// note that entityType.GetProperties() will throw an exception, so we have to use reflection
var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == type && p.SetMethod != null);

foreach (var property in properties)`

Was this page helpful?
0 / 5 - 0 ratings