Entidade Framework DateTime e UTC

É possível ter o Entity Framework (estou usando o Code First Approach com CTP5 atualmente) armazenar todos os valores DateTime como UTC no database?

Ou há talvez uma maneira de especificá-lo no mapeamento, por exemplo, neste para a coluna last_login:

modelBuilder.Entity().Property(x => x.Id).HasColumnName("id"); modelBuilder.Entity().Property(x => x.IsAdmin).HasColumnName("admin"); modelBuilder.Entity().Property(x => x.IsEnabled).HasColumnName("enabled"); modelBuilder.Entity().Property(x => x.PasswordHash).HasColumnName("password_hash"); modelBuilder.Entity().Property(x => x.LastLogin).HasColumnName("last_login"); 

Aqui está uma abordagem que você pode considerar:

Primeiro, defina este atributo a seguir:

 [AttributeUsage(AttributeTargets.Property)] public class DateTimeKindAttribute : Attribute { private readonly DateTimeKind _kind; public DateTimeKindAttribute(DateTimeKind kind) { _kind = kind; } public DateTimeKind Kind { get { return _kind; } } public static void Apply(object entity) { if (entity == null) return; var properties = entity.GetType().GetProperties() .Where(x => x.PropertyType == typeof(DateTime) || x.PropertyType == typeof(DateTime?)); foreach (var property in properties) { var attr = property.GetCustomAttribute(); if (attr == null) continue; var dt = property.PropertyType == typeof(DateTime?) ? (DateTime?) property.GetValue(entity) : (DateTime) property.GetValue(entity); if (dt == null) continue; property.SetValue(entity, DateTime.SpecifyKind(dt.Value, attr.Kind)); } } } 

Agora conecte esse atributo ao seu contexto EF:

 public class MyContext : DbContext { public DbSet Foos { get; set; } public MyContext() { ((IObjectContextAdapter)this).ObjectContext.ObjectMaterialized += (sender, e) => DateTimeKindAttribute.Apply(e.Entity); } } 

Agora em qualquer DateTime ou DateTime? propriedades, você pode aplicar este atributo:

 public class Foo { public int Id { get; set; } [DateTimeKind(DateTimeKind.Utc)] public DateTime Bar { get; set; } } 

Com isso em vigor, sempre que o Entity Framework carregar uma entidade do database, ele definirá o DateTimeKind especificado, como UTC.

Observe que isso não faz nada ao salvar. Você ainda terá que ter o valor convertido corretamente em UTC antes de tentar salvá-lo. Mas ele permite que você defina o tipo ao recuperar, o que permite que ele seja serializado como UTC ou convertido em outros fusos horários com o TimeZoneInfo .

Eu realmente gosto da abordagem de Matt Johnson, mas no meu modelo, todos os meus membros do DateTime são UTC e eu não quero ter que decorar todos eles com um atributo. Então, eu generalizei a abordagem de Matt para permitir que o manipulador de events aplicasse um valor Kind padrão, a menos que um membro fosse explicitamente decorado com o atributo.

O construtor da class ApplicationDbContext inclui este código:

 ///  Constructor: Initializes a new ApplicationDbContext instance.  public ApplicationDbContext() : base(MyApp.ConnectionString, throwIfV1Schema: false) { // Set the Kind property on DateTime variables retrieved from the database ((IObjectContextAdapter)this).ObjectContext.ObjectMaterialized += (sender, e) => DateTimeKindAttribute.Apply(e.Entity, DateTimeKind.Utc); } 

DateTimeKindAttribute parece com isso:

 ///  Sets the DateTime.Kind value on DateTime and DateTime? members retrieved by Entity Framework. Sets Kind to DateTimeKind.Utc by default.  [AttributeUsage(AttributeTargets.Property)] public class DateTimeKindAttribute : Attribute { ///  The DateTime.Kind value to set into the returned value.  public readonly DateTimeKind Kind; ///  Specifies the DateTime.Kind value to set on the returned DateTime value.  ///  The DateTime.Kind value to set on the returned DateTime value.  public DateTimeKindAttribute(DateTimeKind kind) { Kind = kind; } ///  Event handler to connect to the ObjectContext.ObjectMaterialized event.  ///  The entity (POCO class) being materialized.  ///  [Optional] The Kind property to set on all DateTime objects by default.  public static void Apply(object entity, DateTimeKind? defaultKind = null) { if (entity == null) return; // Get the PropertyInfos for all of the DateTime and DateTime? properties on the entity var properties = entity.GetType().GetProperties() .Where(x => x.PropertyType == typeof(DateTime) || x.PropertyType == typeof(DateTime?)); // For each DateTime or DateTime? property on the entity... foreach (var propInfo in properties) { // Initialization var kind = defaultKind; // Get the kind value from the [DateTimekind] attribute if it's present var kindAttr = propInfo.GetCustomAttribute(); if (kindAttr != null) kind = kindAttr.Kind; // Set the Kind property if (kind != null) { var dt = (propInfo.PropertyType == typeof(DateTime?)) ? (DateTime?)propInfo.GetValue(entity) : (DateTime)propInfo.GetValue(entity); if (dt != null) propInfo.SetValue(entity, DateTime.SpecifyKind(dt.Value, kind.Value)); } } } } 

Acredito que encontrei uma solução que não requer nenhuma verificação personalizada de UTC ou manipulação de DateTime.

Basicamente, você precisa alterar suas entidades EF para usar o tipo de dados DateTimeOffset (NOT DateTime). Isso armazenará o fuso horário com o valor de data no database (SQL Server 2015 no meu caso).

Quando o EF Core solicita os dados do database, ele também receberá as informações de fuso horário. Quando você passa esses dados para um aplicativo da web (Angular2 no meu caso), a data é automaticamente convertida para o fuso horário local do navegador, que é o que eu espero.

E quando é passado de volta para o meu servidor, ele é convertido para UTC novamente automaticamente, também como esperado.

Estou pesquisando isso agora e a maioria dessas respostas não é exatamente ótima. Pelo que vejo, não há como dizer ao EF6 que as datas que saem do database estão no formato UTC. Se esse for o caso, a maneira mais simples de garantir que as propriedades DateTime do seu modelo estejam no UTC seria verificar e converter no setter.

Aqui está algum c # como pseudocódigo que descreve o algoritmo

 public DateTime MyUtcDateTime { get { return _myUtcDateTime; } set { if(value.Kind == DateTimeKind.Utc) _myUtcDateTime = value; else if (value.Kind == DateTimeKind.Local) _myUtcDateTime = value.ToUniversalTime(); else _myUtcDateTime = DateTime.SpecifyKind(value, DateTimeKind.Utc); } } 

Os dois primeiros ramos são óbvios. O último contém o molho secreto.

Quando o EF6 cria um modelo a partir de dados carregados do database, DateTimes são DateTimeKind.Unspecified . Se você sabe que suas datas são todas UTC no database, a última ramificação funcionará muito bem para você.

DateTime.Now é sempre DateTimeKind.Local , portanto, o algoritmo acima funciona bem para datas geradas no código. A maior parte do tempo.

Você tem que ser cauteloso, no entanto, como existem outras maneiras DateTimeKind.Unspecified pode entrar em seu código. Por exemplo, você pode desserializar seus modelos a partir de dados JSON e seu sabor de desserializador é padronizado para esse tipo. Cabe a você se proteger contra datas localizadas marcadas como DateTimeKind.Unknown de chegar a esse setter de qualquer pessoa, exceto EF.

Não há como especificar o DataTimeKind no Entity Framework. Você pode decidir converter os valores de data e hora em utc antes de armazenar em db e sempre considerar os dados recuperados de db como UTC. Mas os objects DateTime importados durante a consulta sempre serão “Não especificados”. Você também pode evalualte usando o object DateTimeOffset em vez de DateTime.

Se você tiver o cuidado de passar corretamente nas datas UTC quando definir os valores e tudo o que importa, verifique se o DateTimeKind está definido corretamente quando as entidades são recuperadas do database, veja minha resposta aqui: https://stackoverflow.com/ a / 9386364/279590

Essa resposta funciona com o Entity Framework 6

A resposta aceita não funciona para o object Projetado ou Anônimo. O desempenho também pode ser um problema.

Para conseguir isso, precisamos usar um DbCommandInterceptor , um object fornecido pelo EntityFramework.

Criar Interceptor:

 public class UtcInterceptor : DbCommandInterceptor { public override void ReaderExecuted(DbCommand command, DbCommandInterceptionContext interceptionContext) { base.ReaderExecuted(command, interceptionContext); if (interceptionContext?.Result != null && !(interceptionContext.Result is UtcDbDataReader)) { interceptionContext.Result = new UtcDbDataReader(interceptionContext.Result); } } } 

interceptionContext.Result é DbDataReader, que substituímos pelo nosso

 public class UtcDbDataReader : DbDataReader { private readonly DbDataReader source; public UtcDbDataReader(DbDataReader source) { this.source = source; } public override DateTime GetDateTime(int ordinal) { return DateTime.SpecifyKind(source.GetDateTime(ordinal), DateTimeKind.Utc); } // you need to fill all overrides. Just call the same method on source in all cases public new void Dispose() { source.Dispose(); } public new IDataReader GetData(int ordinal) { return source.GetData(ordinal); } } 

Registre o interceptor em sua DbConfiguration

 internal class MyDbConfiguration : DbConfiguration { protected internal MyDbConfiguration () { AddInterceptor(new UtcInterceptor()); } } 

Finalmente, registre a configuração no seu DbContext

 [DbConfigurationType(typeof(MyDbConfiguration ))] internal class MyDbContext : DbContext { // ... } 

É isso aí. Felicidades.

Por simplicidade, aqui está a implementação completa do DbReader:

 using System; using System.Collections; using System.Data; using System.Data.Common; using System.IO; using System.Threading; using System.Threading.Tasks; namespace MyNameSpace { ///  [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1010:CollectionsShouldImplementGenericInterface")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] public class UtcDbDataReader : DbDataReader { private readonly DbDataReader source; public UtcDbDataReader(DbDataReader source) { this.source = source; } ///  public override int VisibleFieldCount => source.VisibleFieldCount; ///  public override int Depth => source.Depth; ///  public override int FieldCount => source.FieldCount; ///  public override bool HasRows => source.HasRows; ///  public override bool IsClosed => source.IsClosed; ///  public override int RecordsAffected => source.RecordsAffected; ///  public override object this[string name] => source[name]; ///  public override object this[int ordinal] => source[ordinal]; ///  public override bool GetBoolean(int ordinal) { return source.GetBoolean(ordinal); } ///  public override byte GetByte(int ordinal) { return source.GetByte(ordinal); } ///  public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length) { return source.GetBytes(ordinal, dataOffset, buffer, bufferOffset, length); } ///  public override char GetChar(int ordinal) { return source.GetChar(ordinal); } ///  public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length) { return source.GetChars(ordinal, dataOffset, buffer, bufferOffset, length); } ///  public override string GetDataTypeName(int ordinal) { return source.GetDataTypeName(ordinal); } ///  /// Returns datetime with Utc kind ///  public override DateTime GetDateTime(int ordinal) { return DateTime.SpecifyKind(source.GetDateTime(ordinal), DateTimeKind.Utc); } ///  public override decimal GetDecimal(int ordinal) { return source.GetDecimal(ordinal); } ///  public override double GetDouble(int ordinal) { return source.GetDouble(ordinal); } ///  public override IEnumerator GetEnumerator() { return source.GetEnumerator(); } ///  public override Type GetFieldType(int ordinal) { return source.GetFieldType(ordinal); } ///  public override float GetFloat(int ordinal) { return source.GetFloat(ordinal); } ///  public override Guid GetGuid(int ordinal) { return source.GetGuid(ordinal); } ///  public override short GetInt16(int ordinal) { return source.GetInt16(ordinal); } ///  public override int GetInt32(int ordinal) { return source.GetInt32(ordinal); } ///  public override long GetInt64(int ordinal) { return source.GetInt64(ordinal); } ///  public override string GetName(int ordinal) { return source.GetName(ordinal); } ///  public override int GetOrdinal(string name) { return source.GetOrdinal(name); } ///  public override string GetString(int ordinal) { return source.GetString(ordinal); } ///  public override object GetValue(int ordinal) { return source.GetValue(ordinal); } ///  public override int GetValues(object[] values) { return source.GetValues(values); } ///  public override bool IsDBNull(int ordinal) { return source.IsDBNull(ordinal); } ///  public override bool NextResult() { return source.NextResult(); } ///  public override bool Read() { return source.Read(); } ///  public override void Close() { source.Close(); } ///  public override T GetFieldValue(int ordinal) { return source.GetFieldValue(ordinal); } ///  public override Task GetFieldValueAsync(int ordinal, CancellationToken cancellationToken) { return source.GetFieldValueAsync(ordinal, cancellationToken); } ///  public override Type GetProviderSpecificFieldType(int ordinal) { return source.GetProviderSpecificFieldType(ordinal); } ///  public override object GetProviderSpecificValue(int ordinal) { return source.GetProviderSpecificValue(ordinal); } ///  public override int GetProviderSpecificValues(object[] values) { return source.GetProviderSpecificValues(values); } ///  public override DataTable GetSchemaTable() { return source.GetSchemaTable(); } ///  public override Stream GetStream(int ordinal) { return source.GetStream(ordinal); } ///  public override TextReader GetTextReader(int ordinal) { return source.GetTextReader(ordinal); } ///  public override Task IsDBNullAsync(int ordinal, CancellationToken cancellationToken) { return source.IsDBNullAsync(ordinal, cancellationToken); } ///  public override Task ReadAsync(CancellationToken cancellationToken) { return source.ReadAsync(cancellationToken); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1816:CallGCSuppressFinalizeCorrectly")] public new void Dispose() { source.Dispose(); } public new IDataReader GetData(int ordinal) { return source.GetData(ordinal); } } } 

Para aqueles que precisam alcançar a solução @MattJohnson com o framework .net 4 como eu, com a limitação de syntax / método de reflection, é necessário fazer algumas modificações, conforme listado abaixo:

  foreach (var property in properties) { DateTimeKindAttribute attr = (DateTimeKindAttribute) Attribute.GetCustomAttribute(property, typeof(DateTimeKindAttribute)); if (attr == null) continue; var dt = property.PropertyType == typeof(DateTime?) ? (DateTime?)property.GetValue(entity,null) : (DateTime)property.GetValue(entity, null); if (dt == null) continue; //If the value is not null set the appropriate DateTimeKind; property.SetValue(entity, DateTime.SpecifyKind(dt.Value, attr.Kind) ,null); } 

Outra abordagem seria criar uma interface com as propriedades de data e hora, implementá-las nas classs de entidade parciais. Em seguida, use o evento SavingChanges para verificar se o object é do tipo de interface, defina esses valores de data e hora para o que você desejar. Na verdade, se forem criados em datas de modificação, você poderá usar esse evento para preenchê-las.

No meu caso, eu tinha apenas uma tabela com os dados UTC. Aqui está o que eu fiz:

 public partial class MyEntity { protected override void OnPropertyChanged(string property) { base.OnPropertyChanged(property); // ensure that values coming from database are set as UTC // watch out for property name changes! switch (property) { case "TransferDeadlineUTC": if (TransferDeadlineUTC.Kind == DateTimeKind.Unspecified) TransferDeadlineUTC = DateTime.SpecifyKind(TransferDeadlineUTC, DateTimeKind.Utc); break; case "ProcessingDeadlineUTC": if (ProcessingDeadlineUTC.Kind == DateTimeKind.Unspecified) ProcessingDeadlineUTC = DateTime.SpecifyKind(ProcessingDeadlineUTC, DateTimeKind.Utc); default: break; } } }