Entity Framework DateTime y UTC

96

¿Es posible que Entity Framework (estoy usando Code First Approach con CTP5 actualmente) almacene todos los valores de DateTime como UTC en la base de datos?

O tal vez haya una manera de especificarlo en el mapeo, por ejemplo, en este para la columna last_login:

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

Respuestas:

144

Aquí hay un enfoque que podría 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<DateTimeKindAttribute>();
            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 conecte ese atributo a su contexto EF:

public class MyContext : DbContext
{
    public DbSet<Foo> Foos { get; set; }

    public MyContext()
    {
        ((IObjectContextAdapter)this).ObjectContext.ObjectMaterialized +=
            (sender, e) => DateTimeKindAttribute.Apply(e.Entity);
    }
}

Ahora, en cualquier DateTimeo DateTime?propiedades, se 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 de la base de datos, establecerá el DateTimeKindque especifique, como UTC.

Tenga en cuenta que esto no hace nada al guardar. Aún tendrá que convertir el valor correctamente a UTC antes de intentar guardarlo. Pero sí le permite establecer el tipo al recuperar, lo que permite serializarlo como UTC o convertirlo a otras zonas horarias con TimeZoneInfo.

Matt Johnson-Pinta
fuente
7
Si no puede hacer que esto funcione, probablemente se esté perdiendo uno de estos usos: using System; utilizando System.Collections.Generic; utilizando System.ComponentModel.DataAnnotations.Schema; utilizando System.Linq; usando System.Reflection;
Saustrup
7
@Saustrup: encontrará que la mayoría de los ejemplos sobre SO omitirán los usos por brevedad, a menos que sean directamente relevantes para la pregunta. Pero gracias.
Matt Johnson-Pint
4
@MattJohnson sin las declaraciones de uso de @ Saustrup, obtiene algunos errores de compilación inútiles como'System.Array' does not contain a definition for 'Where'
Jacob Eggers
7
Como dijo @SilverSideDown, esto solo funciona con .NET 4.5. He creado algunas extensiones para que sea compatible con .NET 4.0 en gist.github.com/munr/3544bd7fab6615290561 . Otra cosa a tener en cuenta es que esto no funcionará con proyecciones, solo entidades completamente cargadas.
Mun
5
¿Alguna sugerencia sobre cómo hacer esto con proyecciones?
Jafin
32

Realmente me gusta el enfoque de Matt Johnson, pero en mi modelo TODOS mis miembros de DateTime son UTC y no quiero tener que decorarlos a todos con un atributo. Entonces 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:

/// <summary> Constructor: Initializes a new ApplicationDbContext instance. </summary>
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 Se ve como esto:

/// <summary> Sets the DateTime.Kind value on DateTime and DateTime? members retrieved by Entity Framework. Sets Kind to DateTimeKind.Utc by default. </summary>
[AttributeUsage(AttributeTargets.Property)]
public class DateTimeKindAttribute : Attribute
{
    /// <summary> The DateTime.Kind value to set into the returned value. </summary>
    public readonly DateTimeKind Kind;

    /// <summary> Specifies the DateTime.Kind value to set on the returned DateTime value. </summary>
    /// <param name="kind"> The DateTime.Kind value to set on the returned DateTime value. </param>
    public DateTimeKindAttribute(DateTimeKind kind)
    {
        Kind = kind;
    }

    /// <summary> Event handler to connect to the ObjectContext.ObjectMaterialized event. </summary>
    /// <param name="entity"> The entity (POCO class) being materialized. </param>
    /// <param name="defaultKind"> [Optional] The Kind property to set on all DateTime objects by default. </param>
    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<DateTimeKindAttribute>();
            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));
            }
        }
    }
}
Bob.en.Indigo.Health
fuente
1
¡Esta es una extensión muy útil para la respuesta aceptada!
Estudiante
Quizás me esté perdiendo algo, pero ¿cómo se establece de forma predeterminada en DateTimeKind.Utc en lugar de DateTimeKind.Unspecified?
Rhonage
1
@Rhonage Lo siento. El valor predeterminado está configurado en el constructor ApplicationDbContext. Actualicé la respuesta para incluir eso.
Bob.at.Indigo.Health
1
@ Bob.at.AIPsychLab Gracias amigo, mucho más claro ahora. Estaba tratando de averiguar si había algo de Reflexión de peso, pero no, ¡es muy simple!
Rhonage
Esto falla si un modelo tiene un DateTImeatributo sin un método de establecimiento (público). Se sugiere editar. Véase también stackoverflow.com/a/3762475/2279059
Florian Winter
13

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 a DbCommandInterceptor, un objeto proporcionado por EntityFramework.

Crear interceptor:

public class UtcInterceptor : DbCommandInterceptor
{
    public override void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> 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);
    }
}

Registre el interceptor en su DbConfiguration

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

Finalmente, registre la configuración para en su DbContext

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

Eso es. Salud.

Para simplificar, aquí está la implementación completa 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
{
    /// <inheritdoc />
    [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;
        }

        /// <inheritdoc />
        public override int VisibleFieldCount => source.VisibleFieldCount;

        /// <inheritdoc />
        public override int Depth => source.Depth;

        /// <inheritdoc />
        public override int FieldCount => source.FieldCount;

        /// <inheritdoc />
        public override bool HasRows => source.HasRows;

        /// <inheritdoc />
        public override bool IsClosed => source.IsClosed;

        /// <inheritdoc />
        public override int RecordsAffected => source.RecordsAffected;

        /// <inheritdoc />
        public override object this[string name] => source[name];

        /// <inheritdoc />
        public override object this[int ordinal] => source[ordinal];

        /// <inheritdoc />
        public override bool GetBoolean(int ordinal)
        {
            return source.GetBoolean(ordinal);
        }

        /// <inheritdoc />
        public override byte GetByte(int ordinal)
        {
            return source.GetByte(ordinal);
        }

        /// <inheritdoc />
        public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length)
        {
            return source.GetBytes(ordinal, dataOffset, buffer, bufferOffset, length);
        }

        /// <inheritdoc />
        public override char GetChar(int ordinal)
        {
            return source.GetChar(ordinal);
        }

        /// <inheritdoc />
        public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length)
        {
            return source.GetChars(ordinal, dataOffset, buffer, bufferOffset, length);
        }

        /// <inheritdoc />
        public override string GetDataTypeName(int ordinal)
        {
            return source.GetDataTypeName(ordinal);
        }

        /// <summary>
        /// Returns datetime with Utc kind
        /// </summary>
        public override DateTime GetDateTime(int ordinal)
        {
            return DateTime.SpecifyKind(source.GetDateTime(ordinal), DateTimeKind.Utc);
        }

        /// <inheritdoc />
        public override decimal GetDecimal(int ordinal)
        {
            return source.GetDecimal(ordinal);
        }

        /// <inheritdoc />
        public override double GetDouble(int ordinal)
        {
            return source.GetDouble(ordinal);
        }

        /// <inheritdoc />
        public override IEnumerator GetEnumerator()
        {
            return source.GetEnumerator();
        }

        /// <inheritdoc />
        public override Type GetFieldType(int ordinal)
        {
            return source.GetFieldType(ordinal);
        }

        /// <inheritdoc />
        public override float GetFloat(int ordinal)
        {
            return source.GetFloat(ordinal);
        }

        /// <inheritdoc />
        public override Guid GetGuid(int ordinal)
        {
            return source.GetGuid(ordinal);
        }

        /// <inheritdoc />
        public override short GetInt16(int ordinal)
        {
            return source.GetInt16(ordinal);
        }

        /// <inheritdoc />
        public override int GetInt32(int ordinal)
        {
            return source.GetInt32(ordinal);
        }

        /// <inheritdoc />
        public override long GetInt64(int ordinal)
        {
            return source.GetInt64(ordinal);
        }

        /// <inheritdoc />
        public override string GetName(int ordinal)
        {
            return source.GetName(ordinal);
        }

        /// <inheritdoc />
        public override int GetOrdinal(string name)
        {
            return source.GetOrdinal(name);
        }

        /// <inheritdoc />
        public override string GetString(int ordinal)
        {
            return source.GetString(ordinal);
        }

        /// <inheritdoc />
        public override object GetValue(int ordinal)
        {
            return source.GetValue(ordinal);
        }

        /// <inheritdoc />
        public override int GetValues(object[] values)
        {
            return source.GetValues(values);
        }

        /// <inheritdoc />
        public override bool IsDBNull(int ordinal)
        {
            return source.IsDBNull(ordinal);
        }

        /// <inheritdoc />
        public override bool NextResult()
        {
            return source.NextResult();
        }

        /// <inheritdoc />
        public override bool Read()
        {
            return source.Read();
        }

        /// <inheritdoc />
        public override void Close()
        {
            source.Close();
        }

        /// <inheritdoc />
        public override T GetFieldValue<T>(int ordinal)
        {
            return source.GetFieldValue<T>(ordinal);
        }

        /// <inheritdoc />
        public override Task<T> GetFieldValueAsync<T>(int ordinal, CancellationToken cancellationToken)
        {
            return source.GetFieldValueAsync<T>(ordinal, cancellationToken);
        }

        /// <inheritdoc />
        public override Type GetProviderSpecificFieldType(int ordinal)
        {
            return source.GetProviderSpecificFieldType(ordinal);
        }

        /// <inheritdoc />
        public override object GetProviderSpecificValue(int ordinal)
        {
            return source.GetProviderSpecificValue(ordinal);
        }

        /// <inheritdoc />
        public override int GetProviderSpecificValues(object[] values)
        {
            return source.GetProviderSpecificValues(values);
        }

        /// <inheritdoc />
        public override DataTable GetSchemaTable()
        {
            return source.GetSchemaTable();
        }

        /// <inheritdoc />
        public override Stream GetStream(int ordinal)
        {
            return source.GetStream(ordinal);
        }

        /// <inheritdoc />
        public override TextReader GetTextReader(int ordinal)
        {
            return source.GetTextReader(ordinal);
        }

        /// <inheritdoc />
        public override Task<bool> IsDBNullAsync(int ordinal, CancellationToken cancellationToken)
        {
            return source.IsDBNullAsync(ordinal, cancellationToken);
        }

        /// <inheritdoc />
        public override Task<bool> 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);
        }
    }
}
user2397863
fuente
Hasta ahora, esta parece la mejor respuesta. Primero probé la variación del atributo, ya que parecía de menor alcance, pero mis pruebas unitarias fallarían con la burla, ya que el vínculo del evento del constructor no parece saber acerca de las asignaciones de tablas que ocurren en el evento OnModelCreating. ¡Éste tiene mi voto!
El senador
1
¿Por qué estás siguiendo Disposey GetData?
user247702
2
Este código probablemente debería acreditar a @IvanStoev: stackoverflow.com/a/40349051/90287
Rami A.
Desafortunadamente, esto falla si está mapeando datos espaciales
Chris
@ user247702 yea shadowing Dispose es un error, anular Dispose (bool)
user2397863
9

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

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

Cuando EF Core solicita los datos de la base de datos, 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 a UTC nuevamente automáticamente, también como se esperaba.

Moutono
fuente
7
DateTimeOffset no almacena la zona horaria, contrariamente a la percepción común. Almacena un desplazamiento de UTC que representa el valor. El desplazamiento no se puede asignar a la inversa para determinar la zona horaria real desde la que se creó el desplazamiento, lo que hace que el tipo de datos sea casi inútil.
Suncat2000
2
No, pero se puede usar para almacenar un DateTime correctamente: medium.com/@ojb500/in-praise-of-datetimeoffset-e0711f991cba
Carl
1
Solo UTC no necesita una ubicación, porque es igual en todas partes. Si usa algo diferente a UTC, también necesita la ubicación, de lo contrario, la información de la hora es inútil, también al usar datetimeoffset.
Horitsu
@ Suncat2000 Es, con mucho, la forma más sensata de almacenar un punto en el tiempo. Todos los demás tipos de fecha / hora tampoco le dan la zona horaria.
Juan
1
DATETIMEOFFSET hará lo que quería el cartel original: almacenar la fecha y hora como UTC sin tener que realizar ninguna conversión (explícita). @Carl DATETIME, DATETIME2 y DATETIMEOFFSET almacenan correctamente el valor de fecha y hora. Aparte de almacenar adicionalmente un desplazamiento de UTC, DATETIMEOFFSET casi no tiene ninguna ventaja. Lo que usa en su base de datos es su llamada. Solo quería recordar el hecho de que no almacena una zona horaria como mucha gente piensa erróneamente.
Suncat2000
5

No hay forma de especificar DataTimeKind en Entity Framework. Puede decidir convertir los valores de fecha y hora a utc antes de almacenarlos en db y siempre asumir que los datos recuperados de db son UTC. Pero los objetos DateTime materalizados durante la consulta siempre serán "Sin especificar". También puede evaluar usando el objeto DateTimeOffset en lugar de DateTime.

Vijay
fuente
5

Estoy investigando esto en este momento, y la mayoría de estas respuestas no son exactamente buenas. 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 similar a 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 dos primeras ramas son obvias. El último contiene la salsa secreta.

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

DateTime.Nowes siempre 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 cuidado, ya que hay otras formas de DateTimeKind.Unspecifiedcolarse en su código. Por ejemplo, puede deserializar sus modelos a partir de datos JSON, y su sabor de deserializador predeterminado es este tipo. Depende de usted protegerse contra las fechas localizadas marcadas DateTimeKind.Unspecifiedpara que no lleguen a ese colocador desde cualquier persona que no sea EF.

staa99
fuente
6
Como descubrí después de varios años de luchar con este problema, si está asignando o seleccionando campos DateTime en otras estructuras, por ejemplo, un objeto de transferencia de datos, EF ignora los métodos getter y setter. En estos casos, aún debe cambiar Kind a DateTimeKind.Utcdespués de que se generen los resultados. Ejemplo: from o in myContext.Records select new DTO() { BrokenTimestamp = o.BbTimestamp };establece todo Tipo en DateTimeKind.Unspecified.
Suncat2000
1
He estado usando DateTimeOffset con Entity Framework durante un tiempo y si especifica sus entidades EF con un tipo de datos de DateTimeOffset, todas sus consultas EF devolverán las fechas con el desplazamiento de UTC, exactamente como se guarda en la base de datos. Entonces, si cambió su tipo de datos a DateTimeOffset en lugar de DateTime, no necesitaría la solución anterior.
Moutono
¡Es bueno saberlo! Gracias @Moutono
Según el comentario de @ Suncat2000, esto simplemente no funciona y debe eliminarse
Ben Morris
5

Para EF Core , hay una gran discusión sobre este tema en GitHub: https://github.com/dotnet/efcore/issues/4711

Una solución (crédito a Christopher Haws ) que dará como resultado el tratamiento de todas las fechas al almacenarlas / recuperarlas de la base de datos como UTC es agregar lo siguiente al OnModelCreatingmétodo de su DbContextclase:

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

Además, consulte este enlace si desea excluir algunas propiedades de algunas entidades para que no se traten como UTC.

Honza Kalfus
fuente
¡Definitivamente la mejor solución para mí! Gracias
Ben Morris
¿Funciona esto con DateTimeOffset?
Mark Redman
1
@MarkRedman No creo que tenga sentido, porque si tiene un caso de uso legítimo para DateTimeOffset, también desea conservar la información sobre la zona horaria. Consulte docs.microsoft.com/en-us/dotnet/standard/datetime/… o stackoverflow.com/a/14268167/3979621 para saber cuándo elegir entre DateTime y DateTimeOffset.
Honza Kalfus
IsQueryTypeparece haber sido reemplazado por IsKeyLess: github.com/dotnet/efcore/commit/…
Mark Tielemans
4

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

michael.aird
fuente
3

¡Otro año, otra solución! Esto es para EF Core.

Tengo muchas DATETIME2(7)columnas que se asignan DateTimey siempre almacenan UTC. No quiero almacenar un desplazamiento porque si mi código es correcto, el desplazamiento siempre será cero.

Mientras tanto, tengo otras columnas que almacenan valores básicos de fecha y hora de desplazamiento desconocido (proporcionados por los usuarios), por lo que simplemente se almacenan / muestran "tal cual", y no se comparan con nada.

Por lo tanto, necesito una solución que pueda aplicar a columnas específicas.

Defina un método de extensión UsesUtc:

private static DateTime FromCodeToData(DateTime fromCode, string name)
    => fromCode.Kind == DateTimeKind.Utc ? fromCode : throw new InvalidOperationException($"Column {name} only accepts UTC date-time values");

private static DateTime FromDataToCode(DateTime fromData) 
    => fromData.Kind == DateTimeKind.Unspecified ? DateTime.SpecifyKind(fromData, DateTimeKind.Utc) : fromData.ToUniversalTime();

public static PropertyBuilder<DateTime?> UsesUtc(this PropertyBuilder<DateTime?> property)
{
    var name = property.Metadata.Name;
    return property.HasConversion<DateTime?>(
        fromCode => fromCode != null ? FromCodeToData(fromCode.Value, name) : default,
        fromData => fromData != null ? FromDataToCode(fromData.Value) : default
    );
}

public static PropertyBuilder<DateTime> UsesUtc(this PropertyBuilder<DateTime> property)
{
    var name = property.Metadata.Name;
    return property.HasConversion(fromCode => FromCodeToData(fromCode, name), fromData => FromDataToCode(fromData));
}

Esto luego se puede usar en propiedades en la configuración del modelo:

modelBuilder.Entity<CustomerProcessingJob>().Property(x => x.Started).UsesUtc();

Tiene la ventaja menor sobre los atributos de que solo puede aplicarlo a propiedades del tipo correcto.

Tenga en cuenta que asume que los valores de la base de datos están en UTC pero simplemente tienen el error Kind. Por lo tanto, controla los valores que intenta almacenar en la base de datos, lanzando una excepción descriptiva si no son UTC.

Daniel Earwicker
fuente
1
Esta es una gran solución que debería estar más arriba, especialmente ahora que la mayoría de los nuevos desarrollos usarán Core o .NET 5. Puntos imaginarios adicionales para la política de aplicación de UTC: si más personas mantuvieran sus fechas UTC hasta la pantalla del usuario real, apenas tendríamos errores de fecha / hora.
oflahero
1

Para aquellos que necesitan lograr la solución @MattJohnson con .net framework 4 como yo, con una limitación de sintaxis / método de reflexión, se requiere una pequeña modificación como se indica 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);
        }  
Sxc
fuente
1

La solución de Matt Johnson-Pint funciona, pero si se supone que todos sus DateTimes son UTC, crear un atributo sería demasiado tortuoso. Así es como lo simplifiqué:

public class MyContext : DbContext
{
    public DbSet<Foo> Foos { get; set; }

    public MyContext()
    {
        ((IObjectContextAdapter)this).ObjectContext.ObjectMaterialized +=
            (sender, e) => SetDateTimesToUtc(e.Entity);
    }

    private static void SetDateTimesToUtc(object entity)
    {
        if (entity == null)
        {
            return;
        }

        var properties = entity.GetType().GetProperties();
        foreach (var property in properties)
        {
            if (property.PropertyType == typeof(DateTime))
            {
                property.SetValue(entity, DateTime.SpecifyKind((DateTime)property.GetValue(entity), DateTimeKind.Utc));
            }
            else if (property.PropertyType == typeof(DateTime?))
            {
                var value = (DateTime?)property.GetValue(entity);
                if (value.HasValue)
                {
                    property.SetValue(entity, DateTime.SpecifyKind(value.Value, DateTimeKind.Utc));
                }
            }
        }
    }
}
Mielipuoli
fuente
0

Otro enfoque sería crear una interfaz con las propiedades de fecha y hora, implementarlas en las clases de entidad 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 / modifican en fechas determinadas, puede utilizar ese evento para completarlas.

AD.Net
fuente
0

En mi caso, solo tenía una tabla con fecha y hora 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;
        }
    }
}
Ronnie Overby
fuente