Entity Framework DateTime y UTC

¿Es posible tener Entity Framework (estoy usando el primer enfoque de código con CTP5 actualmente) almacenar todos los valores de DateTime como UTC en la base de datos?

¿O tal vez hay una forma de especificarlo en la asignación, por ejemplo en este para la columna 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"); 

Aquí hay un enfoque que puede considerar:

Primero, defina este siguiente atributo:

 [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)); } } } 

Ahora enganche ese atributo a su contexto EF:

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

Ahora en cualquier DateTime o DateTime? propiedades, puede aplicar este atributo:

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

Con esto en su lugar, cada vez que Entity Framework carga una entidad desde la base de datos, configurará el DateTimeKind que especifique, como UTC.

Tenga en cuenta que esto no hace nada al guardar. Aún deberá tener el valor convertido correctamente a UTC antes de intentar guardarlo. Pero le permite establecer el tipo al recuperar, lo que le permite ser serializado como UTC, o convertirse a otras zonas TimeZoneInfo con TimeZoneInfo .

Realmente me gusta el enfoque de Matt Johnson, pero en mi modelo TODOS los miembros de DateTime son UTC y no quiero tener que decorarlos a todos con un atributo. Así que generalicé el enfoque de Matt para permitir que el controlador de eventos aplique un valor Kind predeterminado a menos que un miembro esté explícitamente decorado con el atributo.

El constructor de la clase ApplicationDbContext incluye 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 ve así:

 ///  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)); } } } } 

Creo que encontré una solución que no requiere ninguna comprobación UTC personalizada o manipulación DateTime.

Básicamente, debe cambiar sus entidades EF para utilizar el tipo de datos DateTimeOffset (NO DateTime). Esto almacenará la zona horaria con el valor de fecha en la base de datos (SQL Server 2015 en mi caso).

Cuando EF Core solicite los datos del DB, también recibirá la información de la zona horaria. Cuando pasa estos datos a una aplicación web (Angular2 en mi caso), la fecha se convierte automáticamente a la zona horaria local del navegador, que es lo que espero.

Y cuando se devuelve a mi servidor, se convierte de nuevo en UTC automáticamente, también como se esperaba.

Estoy investigando esto ahora mismo, y la mayoría de estas respuestas no son exactamente geniales. Por lo que puedo ver, no hay forma de decirle a EF6 que las fechas que salen de la base de datos están en formato UTC. Si ese es el caso, la forma más sencilla de asegurarse de que las propiedades DateTime de su modelo estén en UTC sería verificar y convertir en el setter.

Aquí hay un pseudocódigo tipo c # que describe el 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); } } 

Las primeras dos twigs son obvias. El último contiene la salsa secreta.

Cuando EF6 crea un modelo a partir de datos cargados desde la base de datos, DateTimes es DateTimeKind.Unspecified . Si sabe que sus fechas son todas UTC en la base de datos, entonces la última twig funcionará muy bien para usted.

DateTime.Now siempre es DateTimeKind.Local , por lo que el algoritmo anterior funciona bien para las fechas generadas en el código. La mayor parte del tiempo

Sin embargo, debe tener precaución, ya que hay otras maneras en que DateTimeKind.Unspecified puede colarse en su código. Por ejemplo, puede deserializar sus modelos a partir de datos JSON, y su sabor deserializador tiene este valor predeterminado. Depende de usted protegerse contra las fechas localizadas marcadas como DateTimeKind.Unknown por llegar a ese setter de alguien que no sea EF.

No hay forma de especificar DataTimeKind en Entity Framework. Puede decidir convertir los valores de fecha y hora en utc antes de almacenar en db y siempre asumir los datos recuperados de db como UTC. Pero los objetos DateTime materalizados durante la consulta siempre serán “No especificados”. También podría evaluar el uso del objeto DateTimeOffset en lugar de DateTime.

Si tiene cuidado de pasar correctamente las fechas UTC cuando establece los valores y todo lo que le interesa es asegurarse de que DateTimeKind esté configurado correctamente cuando las entidades se recuperan de la base de datos, consulte mi respuesta aquí: https://stackoverflow.com/ a / 9386364/279590

Esta respuesta funciona con Entity Framework 6

La respuesta aceptada no funciona para objetos proyectados o anónimos. El rendimiento también podría ser un problema.

Para lograr esto, necesitamos usar un DbCommandInterceptor , un objeto proporcionado por EntityFramework.

Crear 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 es DbDataReader, que reemplazamos por el nuestro

 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); } } 

Registra el interceptor en tu DbConfiguration

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

Finalmente, registre la configuración en su DbContext

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

Eso es. Aclamaciones.

Para simplificar, aquí está toda la implementación de 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 aquellos que necesitan lograr la solución @MattJohnson con .NET Framework 4 como yo, con syntax de reflexión / limitación de método, se requiere una pequeña modificación como se detalla a continuación:

  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); } 

Otro enfoque sería crear una interfaz con las propiedades de fecha y hora, implementarlas en las clases de entidades parciales. Y luego use el evento SavingChanges para verificar si el objeto es del tipo de interfaz, establezca esos valores de fecha y hora en lo que desee. De hecho, si estos se crean en / modificado en tipo de fechas, puede usar ese evento para poblarlos.

En mi caso, solo tenía una tabla con fechas UTC. Esto es lo que hice:

 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; } } }