Entity Framework DateTime i UTC


98

Czy można mieć Entity Framework (używam obecnie podejścia Code First z CTP5), aby przechowywać wszystkie wartości DateTime jako UTC w bazie danych?

Czy może istnieje sposób, aby określić to w mapowaniu, na przykład w tym dla kolumny 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");

Odpowiedzi:


147

Oto jedno podejście, które możesz rozważyć:

Najpierw zdefiniuj następujący atrybut:

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

Teraz podłącz ten atrybut do kontekstu EF:

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

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

Teraz w dowolnych usługach DateTimelub DateTime?możesz zastosować ten atrybut:

public class Foo
{
    public int Id { get; set; }

    [DateTimeKind(DateTimeKind.Utc)]
    public DateTime Bar { get; set; }
}

Dzięki temu za każdym razem, gdy Entity Framework ładuje jednostkę z bazy danych, ustawi to DateTimeKind, co określisz, na przykład UTC.

Pamiętaj, że to nic nie robi podczas zapisywania. Nadal będziesz musiał poprawnie przekonwertować wartość na UTC, zanim spróbujesz ją zapisać. Ale pozwala ustawić rodzaj podczas pobierania, co pozwala na serializację jako UTC lub konwersję do innych stref czasowych za pomocą TimeZoneInfo.


7
Jeśli nie możesz tego uruchomić, prawdopodobnie brakuje jednego z następujących zastosowań: using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Reflection;
Saustrup,

8
@Saustrup - większość przykładów na SO pomija zastosowania dla zwięzłości, chyba że są one bezpośrednio związane z pytaniem. Ale dzięki.
Matt Johnson-Pint

4
@MattJohnson bez instrukcji using @ Saustrup, otrzymujesz nieprzydatne błędy kompilacji, takie jak'System.Array' does not contain a definition for 'Where'
Jacob Eggers

7
Jak powiedział @SilverSideDown, działa to tylko z .NET 4.5. Stworzyłem kilka rozszerzeń, aby był zgodny z .NET 4.0 na gist.github.com/munr/3544bd7fab6615290561 . Inną rzeczą, na którą należy zwrócić uwagę, jest to, że nie zadziała to z rzutami, tylko w pełni załadowanymi encjami.
Mun,

5
Jakieś sugestie, jak to osiągnąć za pomocą prognoz?
Jafin

33

Bardzo podoba mi się podejście Matta Johnsona, ale w moim modelu WSZYSCY moi członkowie DateTime należą do UTC i nie chcę, aby każdy z nich był dekorowany atrybutem. Więc uogólniłem podejście Matta, aby umożliwić obsłudze zdarzeń zastosowanie domyślnej wartości Kind, chyba że element członkowski jest jawnie ozdobiony atrybutem.

Konstruktor klasy ApplicationDbContext zawiera następujący kod:

/// <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 wygląda tak:

/// <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
To bardzo przydatne rozszerzenie zaakceptowanej odpowiedzi!
Learner

Być może czegoś mi brakuje, ale w jaki sposób to domyślnie DateTimeKind.Utc w przeciwieństwie do DateTimeKind.Unspecified?
Rhonage,

1
@Rhonage Przepraszamy za to. Wartość domyślna jest konfigurowana w konstruktorze ApplicationDbContext. Zaktualizowałem odpowiedź, aby to uwzględnić.
Bob.at.Indigo.Health

1
@ Bob.at.AIPsychLab Dzięki kolego, teraz dużo jaśniej. Próbowałem dowiedzieć się, czy jest jakiś ciężar Refleksja - ale nie, śmiertelnie proste!
Rhonage,

To kończy się niepowodzeniem, jeśli model ma DateTImeatrybut bez (publicznej) metody ustawiającej. Zaproponowana edycja. Zobacz także stackoverflow.com/a/3762475/2279059
Florian Winter

15

Ta odpowiedź działa z Entity Framework 6

Zaakceptowana odpowiedź nie działa dla obiektu projektowanego lub anonimowego. Wydajność też może być problemem.

Aby to osiągnąć, musimy użyć DbCommandInterceptorobiektu dostarczonego przez EntityFramework.

Utwórz przechwytywacz:

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 to DbDataReader, który zastępujemy naszym

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

Zarejestruj przechwytywacz w swoim DbConfiguration

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

Na koniec zarejestruj konfigurację dla swojego DbContext

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

Otóż ​​to. Twoje zdrowie.

Dla uproszczenia oto cała implementacja 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);
        }
    }
}

Jak dotąd wydaje się to najlepszą odpowiedzią. Najpierw wypróbowałem odmianę atrybutu, ponieważ wydawała się mniej zaawansowana, ale moje testy jednostkowe zakończyłyby się niepowodzeniem z mockowaniem, ponieważ powiązanie zdarzenia konstruktora wydaje się nie wiedzieć o mapowaniach tabel, które występują w zdarzeniu OnModelCreating. Ten dostanie mój głos!
Senator

1
Dlaczego obserwujesz Disposei GetData?
user247702

2
Ten kod prawdopodobnie powinien zawierać @IvanStoev: stackoverflow.com/a/40349051/90287
Rami A.

Niestety, to się nie powiedzie, jeśli mapujesz dane przestrzenne
Chris

@ user247702 yea shadowing Dispose is error, override Dispose (bool)
user2397863

10

W przypadku EF Core jest świetna dyskusja na ten temat w serwisie GitHub: https://github.com/dotnet/efcore/issues/4711

Rozwiązaniem (uznanie dla Christophera Hawsa ), które spowoduje traktowanie wszystkich dat podczas przechowywania ich do / pobierania z bazy danych jako UTC, jest dodanie następujących elementów do OnModelCreatingmetody Twojej DbContextklasy:

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

Ponadto, sprawdź ten link , jeśli chcesz wykluczyć pewne właściwości niektórych podmiotów z leczony jako UTC.


Zdecydowanie najlepsze rozwiązanie dla mnie! Dzięki
Ben Morris,

1
@MarkRedman Myślę, że to nie ma sensu, ponieważ jeśli masz uzasadniony przypadek użycia DateTimeOffset, chcesz również zachować informacje o strefie czasowej. Zobacz docs.microsoft.com/en-us/dotnet/standard/datetime/… lub stackoverflow.com/a/14268167/3979621, aby dowiedzieć się, kiedy wybrać między DateTime i DateTimeOffset.
Honza Kalfus

1
IsQueryTypewydaje się, że został zastąpiony przez IsKeyLess: github.com/dotnet/efcore/commit/…
Mark Tielemans

Dlaczego potrzebny jest IsQueryType(lub IsKeyLessteraz) czek?
Piotr Perak

9

Uważam, że znalazłem rozwiązanie, które nie wymaga żadnego niestandardowego sprawdzania UTC ani manipulacji datą i godziną.

Zasadniczo musisz zmienić jednostki EF, aby używały typu danych DateTimeOffset (NOT DateTime). Spowoduje to zapisanie strefy czasowej z wartością daty w bazie danych (w moim przypadku SQL Server 2015).

Gdy EF Core zażąda danych z bazy danych, otrzyma również informacje o strefie czasowej. Kiedy przekazujesz te dane do aplikacji internetowej (w moim przypadku Angular2), data jest automatycznie konwertowana na lokalną strefę czasową przeglądarki, czego oczekuję.

A kiedy jest przesyłany z powrotem na mój serwer, jest ponownie automatycznie konwertowany na UTC, również zgodnie z oczekiwaniami.


8
DateTimeOffset nie przechowuje strefy czasowej, w przeciwieństwie do powszechnego przekonania. Przechowuje przesunięcie względem czasu UTC, które reprezentuje wartość. Przesunięcia nie można odwrócić w celu określenia rzeczywistej strefy czasowej, z której zostało utworzone przesunięcie, przez co typ danych jest prawie bezużyteczny.
Suncat2000

2
Nie, ale można go użyć do prawidłowego przechowywania DateTime: medium.com/@ojb500/in-praise-of-datetimeoffset-e0711f991cba
Carl

1
Tylko UTC nie potrzebuje lokalizacji, ponieważ jest wszędzie taka sama. Jeśli używasz czegoś innego niż UTC, potrzebujesz również lokalizacji, w przeciwnym razie informacja o czasie jest bezużyteczna, również przy używaniu przesunięcia datetime.
Horitsu,

1
DATETIMEOFFSET zrobi to, czego chciał oryginalny plakat: zapisze datę i godzinę jako UTC bez konieczności wykonywania jakiejkolwiek (jawnej) konwersji. @Carl DATETIME, DATETIME2 i DATETIMEOFFSET wszystkie przechowują poprawnie wartość daty i godziny. Oprócz dodatkowego przechowywania przesunięcia z UTC, DATETIMEOFFSET nie ma prawie żadnej korzyści. To, czego używasz w swojej bazie danych, jest twoim wezwaniem. Chciałem tylko doprowadzić do domu, że nie przechowuje strefy czasowej, jak wielu ludzi błędnie myśli.
Suncat2000

1
@ Suncat2000 Zaletą jest to, że możesz wysłać tę datę z interfejsu API do przeglądarki klienta. Kiedy przeglądarka klienta otwiera tę datę, wie, jakie jest przesunięcie z UCT i dlatego jest w stanie przekonwertować je na domyślną datę w systemie, w którym klient je przegląda. Dzięki temu konwersja ze strefy czasowej serwera na strefę czasową przeglądarki odbywa się bez konieczności pisania przez programistę żadnego kodu.
Moutono

6

Badam to teraz i większość z tych odpowiedzi nie jest do końca świetna. Z tego, co widzę, nie sposób powiedzieć EF6, że daty wychodzące z bazy danych są w formacie UTC. W takim przypadku najprostszym sposobem upewnienia się, że właściwości DateTime modelu są w formacie UTC, byłoby zweryfikowanie i przekonwertowanie metody ustawiającej.

Oto pseudokod podobny do języka C #, który opisuje algorytm

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

Pierwsze dwie gałęzie są oczywiste. Ostatni zawiera sekretny sos.

Gdy EF6 tworzy model na podstawie danych załadowanych z bazy danych, DateTimes są DateTimeKind.Unspecified. Jeśli wiesz, że wszystkie twoje daty są w bazie danych UTC, ostatnia gałąź będzie dla ciebie świetna.

DateTime.Nowjest zawsze DateTimeKind.Local, więc powyższy algorytm działa dobrze dla dat wygenerowanych w kodzie. Większość czasu.

Musisz jednak być ostrożny, ponieważ istnieją inne sposoby, DateTimeKind.Unspecifiedaby wkraść się do Twojego kodu. Na przykład możesz deserializować modele z danych JSON, a smak deserializatora będzie domyślnie ustawiony na ten rodzaj. To do Ciebie należy ochrona przed zlokalizowanymi datami oznaczonymi DateTimeKind.Unspecifiedprzed dotarciem do tego setera od kogokolwiek poza EF.


6
Jak się dowiedziałem po kilku latach zmagań z tym problemem, jeśli przypisujesz lub wybierasz pola DateTime do innych struktur, na przykład obiektu transferu danych, EF ignoruje zarówno metody pobierające, jak i ustawiające. W takich przypadkach nadal musisz zmienić rodzaj na DateTimeKind.Utcpo wygenerowaniu wyników. Przykład: from o in myContext.Records select new DTO() { BrokenTimestamp = o.BbTimestamp };ustawia wszystkie rodzaje na DateTimeKind.Unspecified.
Suncat2000

1
Od jakiegoś czasu używam DateTimeOffset z Entity Framework i jeśli określisz jednostki EF z typem danych DateTimeOffset, wszystkie zapytania EF zwrócą daty z przesunięciem z UTC, dokładnie tak, jak jest zapisywane w DB. Więc jeśli zmienisz typ danych na DateTimeOffset zamiast DateTime, nie potrzebujesz powyższego obejścia.
Moutono

Dobrze wiedzieć! Dzięki @Moutono

Zgodnie z komentarzem @ Suncat2000, to po prostu nie działa i powinno zostać usunięte
Ben Morris,

5

Nie ma sposobu, aby określić DataTimeKind w Entity Framework. Możesz zdecydować się na konwersję wartości daty i czasu na utc przed zapisaniem ich do db i zawsze zakładać, że dane pobrane z db są podane jako UTC. Ale obiekty DateTime zmaterializowane podczas zapytania zawsze będą miały wartość „Nieokreślone”. Możesz również evalualte używając obiektu DateTimeOffset zamiast DateTime.


5

Kolejny rok, kolejne rozwiązanie! To jest dla EF Core.

Mam wiele DATETIME2(7)kolumn, które odwzorowują DateTimei zawsze przechowują czas UTC. Nie chcę przechowywać przesunięcia, ponieważ jeśli mój kod jest poprawny, przesunięcie będzie zawsze wynosić zero.

W międzyczasie mam inne kolumny, które przechowują podstawowe wartości daty i czasu o nieznanym przesunięciu (podane przez użytkowników), więc są one po prostu przechowywane / wyświetlane „tak jak jest” i nie są porównywane z niczym.

Dlatego potrzebuję rozwiązania, które mogę zastosować do konkretnych kolumn.

Zdefiniuj metodę rozszerzenia 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));
}

Można to następnie użyć we właściwościach w konfiguracji modelu:

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

Ma niewielką przewagę nad atrybutami, ponieważ można ją zastosować tylko do właściwości odpowiedniego typu.

Zauważ, że zakłada, że ​​wartości z DB są w UTC, ale po prostu mają błędne Kind. W związku z tym kontroluje wartości, które próbujesz przechowywać w bazie danych, zgłaszając opisowy wyjątek, jeśli nie są to UTC.


1
To świetne rozwiązanie, które powinno być wyżej, zwłaszcza teraz, gdy większość nowych programów będzie korzystała z Core lub .NET 5. Dodatkowe wyimaginowane punkty za zasady egzekwowania UTC - jeśli więcej osób zachowa swoje daty UTC aż do faktycznego wyświetlenia użytkownika, nie mielibyśmy prawie żadnych błędów związanych z datą / godziną.
oflahero


1

Dla tych, którzy chcą osiągnąć rozwiązanie @MattJohnson z .NET Framework 4, takim jak ja, z odbiciem składni / ograniczeniem metody, wymaga to niewielkiej modyfikacji, jak podano poniżej:

     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

Rozwiązanie Matta Johnsona-Pinta działa, ale jeśli wszystkie Twoje daty mają być UTC, tworzenie atrybutu byłoby zbyt okrężne. Oto jak to uprościłem:

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

Innym podejściem byłoby utworzenie interfejsu z właściwościami datetime i zaimplementowanie ich w częściowych klasach jednostek. Następnie użyj zdarzenia SavingChanges, aby sprawdzić, czy obiekt jest typu interfejsu, ustaw te wartości daty i godziny na dowolne. W rzeczywistości, jeśli są one tworzone / modyfikowane w określonych datach, możesz użyć tego zdarzenia, aby je wypełnić.


niezły pomysł, ale klasy nie będą używane w anonimowych selekcjach.
John Lord

0

W moim przypadku miałem tylko jedną tabelę z czasami UTC. Oto co zrobiłem:

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

0

Rozwiązania tutaj są przydatne, ale spodziewam się, że wielu podejdzie do tego z problemem, że chcą, aby wszystkie ich czasy danych były dostępne w lokalnej strefie czasowej, ale chcą, aby były przetłumaczone, aby utrwalona wersja została zapisana w UTC.

Istnieją 3 wyzwania, aby to zrealizować:

  1. Odczytywanie danych w formacie UTC i konwersja do formatu lokalnego
  2. Dostosowywanie parametrów zapytania, np. SELECT * From PRODUCT, gdzie SALEDATE <@ 1
  3. Przechowywanie danych w czasie lokalnym w formacie UTC

1. Odczyt danych w formacie UTC i konwersja do formatu lokalnego

W takim przypadku powyższe rozwiązanie oparte na pracy Ivana Stoeva DateTime.Kind ustawiony na nieokreślony, a nie UTC, po załadowaniu z bazy danych zrobi to, czego potrzebujesz.

2. Dostosowanie parametrów zapytania

Podobnie do rozwiązania Ivana dla przechwytywacza, możesz użyć przechwytywacza ReaderExecuting. Dodatkową zaletą jest to, że jest to znacznie łatwiejsze do wdrożenia niż ReaderExecuted.

    public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        foreach (DbParameter dbParameter in command.Parameters)
        {
            if (dbParameter.Value is DateTime dtLocal)
            {
                if (dtLocal.Kind != DateTimeKind.Utc)
                {
                    dbParameter.Value = dtLocal.ToUniversalTime();
                }
            }
        }
        base.ReaderExecuting(command, interceptionContext);
    }

3. Przechowywanie danych w czasie lokalnym w formacie UTC

Chociaż istnieją przechwytywacze zapytań, które wyglądają tak, jakby pomagały w tym miejscu, są wywoływane wiele razy i generują nieoczekiwane wyniki. Najlepszym rozwiązaniem, jakie wymyśliłem, było zastąpienie SaveChanges

    public override int SaveChanges()
    {
        UpdateCommonProperties();
        UpdateDatesToUtc();
        bool saveFailed;
        do
        {
            saveFailed = false;
            try
            {
                var result = base.SaveChanges();
                return result;
            }
            catch (DbUpdateConcurrencyException ex)
            {
                saveFailed = ConcurrencyExceptionHandler(ex);
            }

        } while (saveFailed);
        return 0;
    }

    private void UpdateDatesToUtc()
    {
        if (!ChangeTracker.HasChanges()) return;

        var modifiedEntries = ChangeTracker.Entries().Where(x => (x.State == EntityState.Added || x.State == EntityState.Modified));

        foreach (var entry in modifiedEntries)
        {
            entry.ModifyTypes<DateTime>(ConvertToUtc);
            entry.ModifyTypes<DateTime?>(ConvertToUtc);
        }
    }

    private static DateTime ConvertToUtc(DateTime dt)
    {
        if (dt.Kind == DateTimeKind.Utc) return dt;
        return dt.ToUniversalTime();
    }

    private static DateTime? ConvertToUtc(DateTime? dt)
    {
        if (dt?.Kind == DateTimeKind.Utc) return dt;
        return dt?.ToUniversalTime();
    }

A rozszerzenie to (na podstawie odpowiedzi Talona https://stackoverflow.com/a/39974362/618660

public static class TypeReflectionExtension
{
    static Dictionary<Type, PropertyInfo[]> PropertyInfoCache = new Dictionary<Type, PropertyInfo[]>();

    static void TypeReflectionHelper()
    {
        PropertyInfoCache = new Dictionary<Type, PropertyInfo[]>();
    }

    public static PropertyInfo[] GetTypeProperties(this Type type)
    {
        if (!PropertyInfoCache.ContainsKey(type))
        {
            PropertyInfoCache[type] = type.GetProperties();
        }
        return PropertyInfoCache[type];
    }

    public static void ModifyTypes<T>(this DbEntityEntry dbEntityEntry, Func<T, T> method)
    {
        foreach (var propertyInfo in dbEntityEntry.Entity.GetType().GetTypeProperties().Where(p => p.PropertyType == typeof(T) && p.CanWrite))
        {
            propertyInfo.SetValue(dbEntityEntry.Entity, method(dbEntityEntry.CurrentValues.GetValue<T>(propertyInfo.Name)));
        }
    }
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.