Entity Framework DateTime e UTC


96

É possível fazer com que o Entity Framework (estou usando o Code First Approach com CTP5 atualmente) armazene todos os valores DateTime como UTC no banco de dados?

Ou talvez haja uma maneira de especificá-lo no mapeamento, por exemplo, neste para a coluna 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");

Respostas:


144

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

Agora conecte esse atributo ao seu contexto EF:

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

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

Agora, em qualquer propriedade DateTimeou DateTime?, 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 banco de dados, ele definirá o DateTimeKindque você especificar, como UTC.

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


7
Se você não conseguir fazer isso funcionar, provavelmente está faltando um destes usos: using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; usando System.Linq; using System.Reflection;
Saustrup

7
@Saustrup - você encontrará a maioria dos exemplos de SO que omitem o uso por questões de brevidade, a menos que sejam diretamente relevantes para a questão. Mas obrigado.
Matt Johnson-Pint

4
@MattJohnson sem as instruções de @Saustrup, você obtém alguns erros de compilação inúteis, como'System.Array' does not contain a definition for 'Where'
Jacob Eggers

7
Como @SilverSideDown disse, isso só funciona com o .NET 4.5. Criei algumas extensões para torná-lo compatível com .NET 4.0 em gist.github.com/munr/3544bd7fab6615290561 . Outra coisa a observar é que isso não funcionará com projeções, apenas entidades totalmente carregadas.
Mun

5
Alguma sugestão sobre como fazer isso com projeções?
Jafin

32

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

O construtor da classe ApplicationDbContext inclui 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 parece com isso:

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

1
Esta é uma extensão muito útil para a resposta aceita!
Aprendiz de

Talvez eu esteja faltando alguma coisa, mas como isso é padronizado para DateTimeKind.Utc em oposição a DateTimeKind.Unspecified?
Rhonage

1
@Rhonage Desculpe por isso. O padrão é configurado no construtor ApplicationDbContext. Eu atualizei a resposta para incluir isso.
Bob.at.Indigo.Health

1
@ Bob.at.AIPsychLab Obrigado amigo, muito mais claro agora. Estava tentando descobrir se havia algum peso Reflexão acontecendo - mas não, muito simples!
Rhonage

Isso falhará se um modelo tiver um DateTImeatributo sem um método setter (público). Editar sugerido. Consulte também stackoverflow.com/a/3762475/2279059
Florian Winter

13

Esta resposta funciona com o Entity Framework 6

A resposta aceita não funciona para objetos projetados ou anônimos. O desempenho também pode ser um problema.

Para conseguir isso, precisamos usar a DbCommandInterceptor, um objeto fornecido por EntityFramework.

Criar 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 é 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 seu DbConfiguration

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

Finalmente, registre a configuração para em seu DbContext

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

É isso aí. Felicidades.

Para simplificar, aqui está toda a implementação 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
{
    /// <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);
        }
    }
}

Até agora, esta parece ser a melhor resposta. Eu tentei a variação de atributo primeiro, pois parecia menos abrangente, mas meus testes de unidade falhavam com simulação, já que o vínculo do evento do construtor parece não saber sobre os mapeamentos de tabela que ocorrem no evento OnModelCreating. Este tem meu voto!
O senador

1
Por que você está sombreando Disposee GetData?
user247702

2
Este código provavelmente deve creditar @IvanStoev: stackoverflow.com/a/40349051/90287
Rami A.

Infelizmente, isso falha se você estiver mapeando dados espaciais
Chris,

@ user247702 sim sombreamento Dispose é um erro, ignora Dispose (bool)
user2397863

9

Eu acredito que encontrei uma solução que não requer qualquer verificação UTC personalizada 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 banco de dados (SQL Server 2015 no meu caso).

Quando o EF Core solicita os dados do banco de dados, ele também recebe 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 esperava.

E quando ele é passado de volta para o meu servidor, ele é convertido para UTC novamente de forma automática, também conforme o esperado.


7
DateTimeOffset não armazena o fuso horário, ao contrário da percepção comum. Ele armazena um deslocamento do UTC que o valor representa. O deslocamento não pode ser mapeado ao contrário para determinar o fuso horário real a partir do qual o deslocamento foi criado, tornando o tipo de dados quase inútil.
Suncat2000

2
Não, mas pode ser usado para armazenar um DateTime corretamente: medium.com/@ojb500/in-praise-of-datetimeoffset-e0711f991cba
Carl

1
Apenas o UTC não precisa de um local, porque é o mesmo em todos os lugares. Se você usar algo diferente do UTC, também precisará do local, caso contrário, a informação de tempo será inútil, também no uso de datetimeoffset.
Horitsu

@ Suncat2000 É de longe a maneira mais sensata de armazenar um ponto no tempo. Todos os outros tipos de data / hora também não fornecem o fuso horário.
John

1
DATETIMEOFFSET fará o que o autor da postagem original queria: armazenar data e hora como UTC sem ter que realizar nenhuma conversão (explícita). @Carl DATETIME, DATETIME2 e DATETIMEOFFSET armazenam o valor de data-hora corretamente. Além de armazenar adicionalmente um deslocamento do UTC, DATETIMEOFFSET quase não tem nenhuma vantagem. O que você usa em seu banco de dados é sua chamada. Eu só queria deixar claro que ele não armazena um fuso horário como muitas pessoas pensam erroneamente.
Suncat2000

5

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 assumir os dados recuperados de db como UTC. Mas os objetos DateTime materializados durante a consulta serão sempre "Não especificados". Você também pode avaliar usando o objeto DateTimeOffset em vez de DateTime.


5

Estou pesquisando isso agora, e a maioria dessas respostas não são exatamente boas. Pelo que posso ver, não há como dizer ao EF6 que as datas que saem do banco de dados estão no formato UTC. Se for esse o caso, a maneira mais simples de garantir que as propriedades DateTime do seu modelo estejam em UTC seria verificar e converter no configurador.

Aqui está um pseudocódigo semelhante ao c # 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 primeiros dois ramos são óbvios. O último contém o molho secreto.

Quando EF6 cria um modelo a partir de dados carregados do banco de dados, DateTimes são DateTimeKind.Unspecified. Se você sabe que suas datas estão todas UTC no banco de dados, o último branch 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ê deve ter cuidado, no entanto, pois existem outras maneiras de DateTimeKind.Unspecifiedentrar furtivamente em seu código. Por exemplo, você pode desserializar seus modelos de dados JSON, e seu desserializador padrão para este tipo. Depende de você evitar que datas localizadas marcadas DateTimeKind.Unspecifiedcheguem àquele setter de qualquer pessoa exceto EF.


6
Como descobri depois de vários anos lutando com esse problema, se você estiver atribuindo ou selecionando campos DateTime em outras estruturas, por exemplo, um objeto de transferência de dados, EF ignora os métodos getter e setter. Nesses casos, você ainda precisa alterar o tipo para DateTimeKind.Utcdepois que os resultados forem gerados. Exemplo: from o in myContext.Records select new DTO() { BrokenTimestamp = o.BbTimestamp };define todos os tipos como DateTimeKind.Unspecified.
Suncat2000

1
Estou usando DateTimeOffset com Entity Framework por um tempo e se você especificar suas entidades EF com um tipo de dados de DateTimeOffset, todas as suas consultas EF retornarão as datas com o deslocamento de UTC, exatamente como são salvas no banco de dados. Portanto, se você alterou seu tipo de dados para DateTimeOffset em vez de DateTime, não precisará da solução alternativa acima.
Moutono

É bom Saber! Obrigado @Moutono

De acordo com o comentário de @ Suncat2000, isso simplesmente não funciona e deve ser removido
Ben Morris

5

Para EF Core , há uma grande discussão sobre este tópico no GitHub: https://github.com/dotnet/efcore/issues/4711

Uma solução (crédito para Christopher Haws ) que resultará no tratamento de todas as datas ao armazená-las / recuperá-las do banco de dados como UTC é adicionar o seguinte ao OnModelCreatingmétodo de sua DbContextclasse:

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

Além disso, marque este link se desejar excluir algumas propriedades de algumas entidades de serem tratadas como UTC.


Definitivamente a melhor solução para mim! Obrigado
Ben Morris

Isso funciona com DateTimeOffset?
Mark Redman

1
@MarkRedman Não acho que faça sentido, porque se você tiver um caso de uso legítimo para DateTimeOffset, você deseja manter as informações sobre o fuso horário também. Consulte docs.microsoft.com/en-us/dotnet/standard/datetime/… ou stackoverflow.com/a/14268167/3979621 para saber quando escolher entre DateTime e DateTimeOffset.
Honza Kalfus

IsQueryTypeparece ter sido substituído por IsKeyLess: github.com/dotnet/efcore/commit/…
Mark Tielemans


3

Mais um ano, outra solução! Isso é para EF Core.

Tenho muitas DATETIME2(7)colunas que mapeiam paraDateTime e sempre armazenam UTC. Não quero armazenar um deslocamento porque se meu código estiver correto, o deslocamento sempre será zero.

Enquanto isso, tenho outras colunas que armazenam valores básicos de data e hora de deslocamento desconhecido (fornecidos pelos usuários), de modo que são armazenados / exibidos "como estão" e não são comparados com nada.

Portanto, preciso de uma solução que possa aplicar a colunas específicas.

Defina um método de extensão 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));
}

Isso pode ser usado em propriedades na configuração do modelo:

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

Ele tem a pequena vantagem sobre os atributos de que você só pode aplicá-lo às propriedades do tipo correto.

Observe que ele assume que os valores do banco de dados estão em UTC, mas apenas estão errados Kind. Portanto, ele policia os valores que você tenta armazenar no BD, lançando uma exceção descritiva se eles não forem UTC.


1
Esta é uma ótima solução que deve ser superior, especialmente agora que a maioria dos novos desenvolvimentos usará Core ou .NET 5. Pontos imaginários de bônus para a política de aplicação de UTC - se mais pessoas mantivessem suas datas UTC até a exibição do usuário real, dificilmente teríamos erros de data / hora.
oflahero

1

Para aqueles que precisam alcançar a solução @MattJohnson com .net framework 4 como eu, com sintaxe de reflexão / limitação de método, é necessária uma pequena modificação 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);
        }  

1

A solução de Matt Johnson-Pint funciona, mas se todos os seus DateTimes deveriam ser UTC, criar um atributo seria muito tortuoso. Aqui está como eu simplifiquei:

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

0

Outra abordagem seria criar uma interface com as propriedades datetime e implementá-las nas classes de entidade parciais. Em seguida, use o evento SavingChanges para verificar se o objeto é do tipo de interface, defina esses valores de data e hora para o que quiser. Na verdade, se eles forem criados / modificados em datas, você pode usar esse evento para preenchê-los.


0

No meu caso, eu tinha apenas uma tabela com datetimes 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;
        }
    }
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.