Entity Framework DateTime e UTC


96

È possibile che Entity Framework (attualmente utilizzo l'approccio Code First con CTP5) archivi tutti i valori DateTime come UTC nel database?

O forse c'è un modo per specificarlo nella mappatura, ad esempio in questa per la colonna 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");

Risposte:


144

Ecco un approccio che potresti prendere in considerazione:

Per prima cosa, definisci questo attributo seguente:

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

Ora collega quell'attributo al tuo contesto EF:

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

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

Ora su qualsiasi DateTimeo DateTime?proprietà, puoi applicare questo attributo:

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

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

Con questa impostazione, ogni volta che Entity Framework carica un'entità dal database, imposterà il valore DateTimeKindspecificato, ad esempio UTC.

Nota che questo non fa nulla durante il salvataggio. Dovrai comunque convertire correttamente il valore in UTC prima di provare a salvarlo. Ma ti consente di impostare il tipo durante il recupero, il che consente di serializzarlo come UTC o convertirlo in altri fusi orari con TimeZoneInfo.


7
Se non riesci a farlo funzionare, probabilmente ti manca uno di questi usi: using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Reflection;
Saustrup

7
@ Saustrup - Troverai che la maggior parte degli esempi su SO ometterà gli usi per brevità, a meno che non siano direttamente pertinenti alla domanda. Ma grazie.
Matt Johnson-Pint

4
@ MattJohnson senza le istruzioni using di @ Saustrup, si ottengono alcuni errori di compilazione inutili come'System.Array' does not contain a definition for 'Where'
Jacob Eggers,

7
Come ha detto @SilverSideDown, funziona solo con .NET 4.5. Ho creato alcune estensioni per renderlo compatibile con .NET 4.0 su gist.github.com/munr/3544bd7fab6615290561 . Un'altra cosa da notare è che questo non funzionerà con le proiezioni, solo con le entità completamente caricate.
Mun

5
Qualche suggerimento su come ottenere questo risultato con le proiezioni?
Jafin

32

Mi piace molto l'approccio di Matt Johnson, ma nel mio modello TUTTI i membri di DateTime sono UTC e non voglio dover decorarli tutti con un attributo. Quindi ho generalizzato l'approccio di Matt per consentire al gestore di eventi di applicare un valore Kind predefinito a meno che un membro non sia esplicitamente decorato con l'attributo.

Il costruttore per la classe ApplicationDbContext include questo codice:

/// <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 Somiglia a questo:

/// <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
Questa è un'estensione molto utile alla risposta accettata!
Studente

Forse mi manca qualcosa, ma come funziona questo valore predefinito su DateTimeKind.Utc rispetto a DateTimeKind.Unspecified?
Rhonage

1
@Rhonage Mi dispiace per questo. Il valore predefinito è impostato nel costruttore ApplicationDbContext. Ho aggiornato la risposta per includerla.
Bob.at.Indigo.Health

1
@ Bob.at.AIPsychLab Grazie amico, molto più chiaro ora. Stavo cercando di capire se ci fosse qualche riflessione sul peso in corso - ma no, semplicissimo!
Rhonage

Questo fallisce se un modello ha un DateTImeattributo senza un metodo setter (pubblico). Modifica suggerita. Vedi anche stackoverflow.com/a/3762475/2279059
Florian Winter

13

Questa risposta funziona con Entity Framework 6

La risposta accettata non funziona per l'oggetto proiettato o anonimo. Anche le prestazioni potrebbero essere un problema.

Per ottenere ciò, dobbiamo utilizzare a DbCommandInterceptor, un oggetto fornito da EntityFramework.

Crea intercettore:

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, che sostituiamo con il nostro

public class UtcDbDataReader : DbDataReader
{
    private readonly DbDataReader source;

    public UtcDbDataReader(DbDataReader source)
    {
        this.source = source;
    }

    public override DateTime GetDateTime(int ordinal)
    {
        return DateTime.SpecifyKind(source.GetDateTime(ordinal), DateTimeKind.Utc);
    }        

    // you need to fill all overrides. Just call the same method on source in all cases

    public new void Dispose()
    {
        source.Dispose();
    }

    public new IDataReader GetData(int ordinal)
    {
        return source.GetData(ordinal);
    }
}

Registra l'interceptor nel tuo file DbConfiguration

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

Infine, registra la configurazione per sul tuo DbContext

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

Questo è tutto. Saluti.

Per semplicità, ecco l'intera implementazione di 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);
        }
    }
}

Finora questa sembra la migliore risposta. Ho provato prima la variazione dell'attributo perché sembrava di portata minore, ma i miei test unitari avrebbero fallito con il mocking poiché il tie-in dell'evento del costruttore non sembra conoscere i mapping delle tabelle che si verificano nell'evento OnModelCreating. Questo ottiene il mio voto!
Il senatore

1
Perché stai pedinando Disposee GetData?
user247702

2
Questo codice dovrebbe probabilmente @IvanStoev credito: stackoverflow.com/a/40349051/90287
Rami A.

Sfortunatamente questo non riesce se stai mappando i dati spaziali
Chris

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

9

Credo di aver trovato una soluzione che non richiede alcun controllo UTC personalizzato o manipolazione di DateTime.

Fondamentalmente è necessario modificare le entità EF per utilizzare il tipo di dati DateTimeOffset (NON DateTime). Ciò memorizzerà il fuso orario con il valore della data nel database (SQL Server 2015 nel mio caso).

Quando EF Core richiede i dati dal DB, riceverà anche le informazioni sul fuso orario. Quando passi questi dati a un'applicazione web (Angular2 nel mio caso) la data viene automaticamente convertita nel fuso orario locale del browser, che è quello che mi aspetto.

E quando viene passato di nuovo al mio server, viene nuovamente convertito in UTC automaticamente, anche come previsto.


7
DateTimeOffset non memorizza il fuso orario, contrariamente alla percezione comune. Memorizza un offset da UTC rappresentato dal valore. L'offset non può essere mappato al contrario per determinare il fuso orario effettivo da cui è stato creato l'offset, rendendo così il tipo di dati quasi inutile.
Suncat2000

2
No, ma può essere utilizzato per memorizzare correttamente un DateTime: medium.com/@ojb500/in-praise-of-datetimeoffset-e0711f991cba
Carl

1
Solo UTC non ha bisogno di una posizione, perché è ovunque la stessa. Se usi qualcosa di diverso dall'UTC hai bisogno anche della posizione, altrimenti l'informazione dell'ora è inutile, anche quando usi datetimeoffset.
Horitsu

@ Suncat2000 È di gran lunga il modo più sensato per memorizzare un punto nel tempo. Tutti gli altri tipi di data / ora non ti danno neanche il fuso orario.
John

1
DATETIMEOFFSET farà quello che voleva il poster originale: memorizza la data e l'ora come UTC senza dover eseguire alcuna conversione (esplicita). @Carl DATETIME, DATETIME2 e DATETIMEOFFSET memorizzano tutti correttamente il valore data-ora. Oltre alla memorizzazione aggiuntiva di un offset da UTC, DATETIMEOFFSET non ha quasi alcun vantaggio. Ciò che usi nel tuo database è la tua chiamata. Volevo solo portare a casa il punto che non memorizza un fuso orario come molte persone pensano erroneamente.
Suncat2000

5

Non è possibile specificare DataTimeKind in Entity Framework. È possibile decidere di convertire i valori di data e ora in utc prima di archiviarli in db e assumere sempre i dati recuperati da db come UTC. Ma gli oggetti DateTime resi disponibili durante la query saranno sempre "Unspecified". Puoi anche valutare utilizzando l'oggetto DateTimeOffset invece di DateTime.


5

Sto cercando questo in questo momento e la maggior parte di queste risposte non sono esattamente eccezionali. Da quello che posso vedere, non c'è modo di dire a EF6 che le date che escono dal database sono in formato UTC. In tal caso, il modo più semplice per assicurarsi che le proprietà DateTime del modello siano in UTC sarebbe verificare e convertire nel setter.

Ecco uno pseudocodice simile a c # che descrive l'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);        
    }    
}

I primi due rami sono evidenti. L'ultimo detiene la salsa segreta.

Quando EF6 crea un modello dai dati caricati dal database, DateTimes sono DateTimeKind.Unspecified. Se sai che le tue date sono tutte UTC nel db, l'ultimo ramo funzionerà alla grande per te.

DateTime.Nowè sempre DateTimeKind.Local, quindi l'algoritmo di cui sopra funziona bene per le date generate nel codice. La maggior parte delle volte.

Devi essere cauto, tuttavia, poiché ci sono altri modi per DateTimeKind.Unspecifiedintrufolarsi nel tuo codice. Ad esempio, potresti deserializzare i tuoi modelli dai dati JSON e l'impostazione predefinita del tuo deserializzatore è questo tipo. Sta a te proteggerti dalle date localizzate contrassegnate DateTimeKind.Unspecifieddall'arrivo a quel setter da chiunque tranne EF.


6
Come ho scoperto dopo diversi anni di lotta con questo problema, se si assegnano o si selezionano i campi DateTime in altre strutture, ad esempio un oggetto di trasferimento dati, EF ignora entrambi i metodi getter e setter. In questi casi, devi ancora cambiare Tipo in DateTimeKind.Utcdopo che i risultati sono stati generati. Esempio: from o in myContext.Records select new DTO() { BrokenTimestamp = o.BbTimestamp };imposta tutti i tipi su DateTimeKind.Unspecified.
Suncat2000

1
Uso DateTimeOffset con Entity Framework da un po 'di tempo e se specifichi le tue entità EF con un tipo di dati DateTimeOffset, tutte le tue query EF restituiranno le date con l'offset da UTC, esattamente come vengono salvate nel DB. Quindi, se hai modificato il tipo di dati in DateTimeOffset invece di DateTime, non avresti bisogno della soluzione alternativa precedente.
Moutono

È bello sapere! Grazie @Moutono

Secondo il commento di @ Suncat2000, questo semplicemente non funziona e dovrebbe essere rimosso
Ben Morris

5

Per EF Core , c'è una grande discussione su questo argomento su GitHub: https://github.com/dotnet/efcore/issues/4711

Una soluzione (credito a Christopher Haws ) che comporterà il trattamento di tutte le date durante l'archiviazione / il recupero dal database come UTC consiste nell'aggiungere quanto segue al OnModelCreatingmetodo della 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);
        }
    }
}

Inoltre, controlla questo collegamento se desideri escludere alcune proprietà di alcune entità dal trattamento come UTC.


Sicuramente la soluzione migliore per me! Grazie
Ben Morris

Funziona con DateTimeOffset?
Mark Redman

1
@MarkRedman Non penso abbia senso, perché se hai un caso d'uso legittimo per DateTimeOffset, vuoi mantenere anche le informazioni sul fuso orario. Vedere docs.microsoft.com/en-us/dotnet/standard/datetime/... o stackoverflow.com/a/14268167/3979621 per quando scegliere tra DateTime e DateTimeOffset.
Honza Kalfus

IsQueryTypesembra essere stato sostituito da IsKeyLess: github.com/dotnet/efcore/commit/…
Mark Tielemans


3

Un altro anno, un'altra soluzione! Questo è per EF Core.

Ho molte DATETIME2(7)colonne che mappano DateTimee memorizzo sempre UTC. Non voglio memorizzare un offset perché se il mio codice è corretto, l'offset sarà sempre zero.

Nel frattempo ho altre colonne che memorizzano i valori di data-ora di base di offset sconosciuto (fornito dagli utenti), quindi vengono semplicemente memorizzati / visualizzati "così come sono" e non confrontati con nulla.

Pertanto ho bisogno di una soluzione che posso applicare a colonne specifiche.

Definisci un metodo di estensione 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));
}

Questo può quindi essere utilizzato sulle proprietà nella configurazione del modello:

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

Ha il vantaggio minore rispetto agli attributi che puoi applicarlo solo alle proprietà del tipo corretto.

Si noti che presume che i valori del DB siano in UTC ma che abbiano solo errori Kind. Pertanto controlla i valori che si tenta di memorizzare nel DB, generando un'eccezione descrittiva se non sono UTC.


1
Questa è un'ottima soluzione che dovrebbe essere più in alto, specialmente ora che la maggior parte dei nuovi sviluppi utilizzerà Core o .NET 5. Punti immaginari bonus per la politica di applicazione UTC - se più persone mantengono le loro date UTC fino alla visualizzazione dell'utente reale, avremmo pochissimi bug data / ora.
oflahero

1

Per coloro che hanno bisogno di ottenere la soluzione @MattJohnson con .net framework 4 come me, con limitazione della sintassi / metodo di riflessione, richiede una piccola modifica come elencato di seguito:

     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

La soluzione di Matt Johnson-Pint funziona, ma se tutti i tuoi DateTimes dovrebbero essere UTC, la creazione di un attributo sarebbe troppo tortuosa. Ecco come l'ho semplificato:

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

Un altro approccio potrebbe essere quello di creare un'interfaccia con le proprietà datetime, implementarle sulle classi di entità parziali. Quindi usa l'evento SavingChanges per verificare se l'oggetto è del tipo di interfaccia, imposta quei valori datetime su quello che vuoi. Infatti, se questi vengono creati / modificati in un tipo di date, puoi utilizzare quell'evento per popolarli.


0

Nel mio caso, avevo solo una tabella con data e ora UTC. Ecco cosa ho fatto:

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;
        }
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.