Entity Framework DateTime et UTC

96

Est-il possible qu'Entity Framework (j'utilise actuellement l'approche Code First avec CTP5) stocke toutes les valeurs DateTime au format UTC dans la base de données?

Ou y a-t-il peut-être un moyen de le spécifier dans le mappage, par exemple dans celui-ci pour la colonne last_login:

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

Réponses:

144

Voici une approche que vous pourriez envisager:

Tout d'abord, définissez cet attribut suivant:

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

Maintenant, connectez cet attribut à votre contexte EF:

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

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

Maintenant, sur n'importe quelle propriété DateTimeou DateTime?, vous pouvez appliquer cet attribut:

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

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

Avec cela en place, chaque fois qu'Entity Framework charge une entité à partir de la base de données, il définit le DateTimeKindque vous spécifiez, tel que UTC.

Notez que cela ne fait rien lors de l'enregistrement. Vous devrez toujours avoir la valeur correctement convertie en UTC avant d'essayer de l'enregistrer. Mais il vous permet de définir le genre lors de la récupération, ce qui lui permet d'être sérialisé au format UTC ou de le convertir dans d'autres fuseaux horaires avec TimeZoneInfo.

Matt Johnson-Pint
la source
7
Si cela ne fonctionne pas, il vous manque probablement l'une de ces utilisations: using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Reflection;
Saustrup le
7
@Saustrup - Vous trouverez la plupart des exemples sur SO omettra les utilisations par souci de concision, à moins qu'elles ne soient directement pertinentes pour la question. Mais merci.
Matt Johnson-Pint
4
@MattJohnson sans les instructions using de @ Saustrup, vous obtenez des erreurs de compilation inutiles telles que'System.Array' does not contain a definition for 'Where'
Jacob Eggers
7
Comme l'a dit @SilverSideDown, cela ne fonctionne qu'avec .NET 4.5. J'ai créé des extensions pour le rendre compatible avec .NET 4.0 sur gist.github.com/munr/3544bd7fab6615290561 . Une autre chose à noter est que cela ne fonctionnera pas avec des projections, uniquement des entités entièrement chargées.
Mun
5
Des suggestions pour faire avancer les choses avec des projections?
Jafin
32

J'aime vraiment l'approche de Matt Johnson, mais dans mon modèle, TOUS mes membres DateTime sont UTC et je ne veux pas avoir à les décorer tous avec un attribut. J'ai donc généralisé l'approche de Matt pour permettre au gestionnaire d'événements d'appliquer une valeur Kind par défaut à moins qu'un membre ne soit explicitement décoré avec l'attribut.

Le constructeur de la classe ApplicationDbContext inclut ce code:

/// <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 ressemble à ça:

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

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

    /// <summary> Event handler to connect to the ObjectContext.ObjectMaterialized event. </summary>
    /// <param name="entity"> The entity (POCO class) being materialized. </param>
    /// <param name="defaultKind"> [Optional] The Kind property to set on all DateTime objects by default. </param>
    public static void Apply(object entity, DateTimeKind? defaultKind = null)
    {
        if (entity == null) return;

        // Get the PropertyInfos for all of the DateTime and DateTime? properties on the entity
        var properties = entity.GetType().GetProperties()
            .Where(x => x.PropertyType == typeof(DateTime) || x.PropertyType == typeof(DateTime?));

        // For each DateTime or DateTime? property on the entity...
        foreach (var propInfo in properties) {
            // Initialization
            var kind = defaultKind;

            // Get the kind value from the [DateTimekind] attribute if it's present
            var kindAttr = propInfo.GetCustomAttribute<DateTimeKindAttribute>();
            if (kindAttr != null) kind = kindAttr.Kind;

            // Set the Kind property
            if (kind != null) {
                var dt = (propInfo.PropertyType == typeof(DateTime?))
                    ? (DateTime?)propInfo.GetValue(entity)
                    : (DateTime)propInfo.GetValue(entity);

                if (dt != null) propInfo.SetValue(entity, DateTime.SpecifyKind(dt.Value, kind.Value));
            }
        }
    }
}
Bob.at.Indigo.Health
la source
1
C'est une extension très utile à la réponse acceptée!
Apprenant
Il me manque peut-être quelque chose, mais comment cette valeur par défaut est-elle DateTimeKind.Utc par opposition à DateTimeKind.Unspecified?
Rhonage
1
@Rhonage Désolé pour ça. La valeur par défaut est configurée dans le constructeur ApplicationDbContext. J'ai mis à jour la réponse pour inclure cela.
Bob.at.Indigo.Health
1
@ Bob.at.AIPsychLab Merci mon pote, beaucoup plus clair maintenant. J'essayais de savoir s'il y avait une réflexion sur le poids - mais non, tout simplement!
Rhonage
Cela échoue si un modèle a un DateTImeattribut sans méthode de définition (publique). Modification suggérée. Voir aussi stackoverflow.com/a/3762475/2279059
Florian Winter
13

Cette réponse fonctionne avec Entity Framework 6

La réponse acceptée ne fonctionne pas pour l'objet projeté ou anonyme. Les performances peuvent également être un problème.

Pour y parvenir, nous devons utiliser un DbCommandInterceptor , un objet fourni par EntityFramework.

Créer un intercepteur:

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 est DbDataReader, que nous remplaçons par le nôtre

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

Enregistrez l'intercepteur dans votre DbConfiguration

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

Enfin, enregistrez la configuration sur votre DbContext

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

C'est tout. À votre santé.

Pour plus de simplicité, voici l'intégralité de l'implémentation de DbReader:

using System;
using System.Collections;
using System.Data;
using System.Data.Common;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace MyNameSpace
{
    /// <inheritdoc />
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1010:CollectionsShouldImplementGenericInterface")]
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
    public class UtcDbDataReader : DbDataReader
    {
        private readonly DbDataReader source;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        /// <inheritdoc />
        public override Task<bool> ReadAsync(CancellationToken cancellationToken)
        {
            return source.ReadAsync(cancellationToken);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1816:CallGCSuppressFinalizeCorrectly")]
        public new void Dispose()
        {
            source.Dispose();
        }

        public new IDataReader GetData(int ordinal)
        {
            return source.GetData(ordinal);
        }
    }
}
user2397863
la source
Jusqu'à présent, cela semble être la meilleure réponse. J'ai d'abord essayé la variante d'attribut car elle semblait moins loin, mais mes tests unitaires échoueraient avec la moquerie car le lien d'événement du constructeur ne semble pas connaître les mappages de table qui se produisent dans l'événement OnModelCreating. Celui-ci obtient mon vote!
The Senator
1
Pourquoi tu fais de l'ombre Disposeet GetData?
user247702
2
Ce code devrait probablement créditer @IvanStoev: stackoverflow.com/a/40349051/90287
Rami A.
Malheureusement, cela échoue si vous mappez des données spatiales
Chris
@ user247702 yea shadowing Dispose is error, override Dispose (bool)
user2397863
9

Je crois avoir trouvé une solution qui ne nécessite aucune vérification UTC personnalisée ou manipulation de DateTime.

Fondamentalement, vous devez modifier vos entités EF pour utiliser le type de données DateTimeOffset (PAS DateTime). Cela stockera le fuseau horaire avec la valeur de date dans la base de données (SQL Server 2015 dans mon cas).

Lorsque EF Core demande les données à la base de données, il recevra également les informations de fuseau horaire. Lorsque vous transmettez ces données à une application Web (Angular2 dans mon cas), la date est automatiquement convertie dans le fuseau horaire local du navigateur, ce que j'attends.

Et quand il est renvoyé à mon serveur, il est à nouveau converti en UTC automatiquement, également comme prévu.

Moutono
la source
7
DateTimeOffset ne stocke pas le fuseau horaire, contrairement à la perception courante. Il stocke un décalage par rapport à UTC que la valeur représente. Le décalage ne peut pas être mappé en sens inverse pour déterminer le fuseau horaire réel à partir duquel le décalage a été créé, rendant ainsi le type de données presque inutile.
Suncat2000
2
Non, mais il peut être utilisé pour stocker correctement un DateTime: medium.com/@ojb500/in-praise-of-datetimeoffset-e0711f991cba
Carl
1
Seul UTC n'a pas besoin d'un emplacement, car il est partout le même. Si vous utilisez autre chose que UTC, vous avez également besoin de l'emplacement, sinon les informations de temps sont inutiles, également pour utiliser datetimeoffset.
Horitsu
@ Suncat2000 C'est de loin le moyen le plus judicieux de stocker un point dans le temps. Tous les autres types de date / heure ne vous donnent pas non plus le fuseau horaire.
John
1
DATETIMEOFFSET fera ce que l'affiche originale voulait: stocker la date-heure au format UTC sans avoir à effectuer de conversion (explicite). @Carl DATETIME, DATETIME2 et DATETIMEOFFSET stockent tous correctement la valeur date-heure. Outre le stockage supplémentaire d'un décalage par rapport à UTC, DATETIMEOFFSET n'a pratiquement aucun avantage. Ce que vous utilisez dans votre base de données est votre appel. Je voulais juste souligner le fait qu'il ne stocke pas de fuseau horaire comme beaucoup de gens le pensent à tort.
Suncat2000
5

Il n'existe aucun moyen de spécifier le DataTimeKind dans Entity Framework. Vous pouvez décider de convertir les valeurs de date et d'heure en utc avant de les stocker en db et de toujours supposer que les données récupérées de db sont UTC. Mais les objets DateTime matérialisés lors de la requête seront toujours "non spécifiés". Vous pouvez également évaluer à l'aide de l'objet DateTimeOffset au lieu de DateTime.

Vijay
la source
5

Je fais des recherches là-dessus en ce moment, et la plupart de ces réponses ne sont pas vraiment excellentes. D'après ce que je peux voir, il n'y a aucun moyen de dire à EF6 que les dates sortant de la base de données sont au format UTC. Si tel est le cas, le moyen le plus simple de s'assurer que les propriétés DateTime de votre modèle sont en UTC serait de vérifier et de convertir dans le setter.

Voici un pseudocode de type c # qui décrit l'algorithme

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

Les deux premières branches sont évidentes. Le dernier détient la sauce secrète.

Lorsque EF6 crée un modèle à partir de données chargées à partir de la base de données, les DateTimes sont DateTimeKind.Unspecified. Si vous savez que vos dates sont toutes UTC dans la base de données, alors la dernière branche fonctionnera très bien pour vous.

DateTime.Now est toujours DateTimeKind.Local , donc l'algorithme ci-dessus fonctionne bien pour les dates générées dans le code. Le plus souvent.

Vous devez cependant être prudent, car il existe d'autres moyens de DateTimeKind.Unspecifiedse faufiler dans votre code. Par exemple, vous pouvez désérialiser vos modèles à partir de données JSON, et votre saveur de désérialiseur est par défaut de ce type. C'est à vous de vous prémunir contre les dates localisées marquées DateTimeKind.Unspecifiedpour arriver à ce passeur par quelqu'un d'autre que EF.

staa99
la source
6
Comme je l'ai découvert après plusieurs années de lutte avec ce problème, si vous attribuez ou sélectionnez des champs DateTime dans d'autres structures, par exemple un objet de transfert de données, EF ignore à la fois les méthodes getter et setter. Dans ces cas, vous devez toujours changer Kind en une DateTimeKind.Utcfois vos résultats générés. Exemple: from o in myContext.Records select new DTO() { BrokenTimestamp = o.BbTimestamp };définit tous Kind sur DateTimeKind.Unspecified.
Suncat2000
1
J'utilise DateTimeOffset avec Entity Framework depuis un certain temps et si vous spécifiez vos entités EF avec un type de données DateTimeOffset, toutes vos requêtes EF renverront les dates avec le décalage par rapport à UTC, exactement comme elles sont enregistrées dans la base de données. Donc, si vous avez changé votre type de données en DateTimeOffset au lieu de DateTime, vous n'avez pas besoin de la solution de contournement ci-dessus.
Moutono
C'est bon à savoir! Merci @Moutono
Selon le commentaire de @ Suncat2000, cela ne fonctionne tout simplement pas et devrait être supprimé
Ben Morris
5

Pour EF Core , il y a une excellente discussion sur ce sujet sur GitHub: https://github.com/dotnet/efcore/issues/4711

Une solution (crédit à Christopher Haws ) qui se traduira par le traitement de toutes les dates lors de leur stockage / récupération de la base de données comme UTC consiste à ajouter ce qui suit à la OnModelCreatingméthode de votre 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);
        }
    }
}

En outre, vérifiez ce lien si vous voulez exclure certaines propriétés de certaines entités d'être traitées comme UTC.

Honza Kalfus
la source
Certainement la meilleure solution pour moi! Merci
Ben Morris
Cela fonctionne-t-il avec DateTimeOffset?
Mark Redman
1
@MarkRedman Je ne pense pas que cela ait du sens, car si vous avez un cas d'utilisation légitime pour DateTimeOffset, vous souhaitez également conserver les informations sur le fuseau horaire. Voir docs.microsoft.com/en-us/dotnet/standard/datetime/… ou stackoverflow.com/a/14268167/3979621 pour savoir quand choisir entre DateTime et DateTimeOffset.
Honza Kalfus
IsQueryTypesemble avoir été remplacé par IsKeyLess: github.com/dotnet/efcore/commit/…
Mark Tielemans
4

Si vous prenez soin de passer correctement les dates UTC lorsque vous définissez les valeurs et que tout ce qui vous importe est de vous assurer que DateTimeKind est correctement défini lorsque les entités sont extraites de la base de données, voir ma réponse ici: https://stackoverflow.com/ a / 9386364/279590

michael.aird
la source
3

Une autre année, une autre solution! C'est pour EF Core.

J'ai beaucoup de DATETIME2(7)colonnes qui correspondent àDateTime et stockent toujours UTC. Je ne veux pas stocker de décalage car si mon code est correct, le décalage sera toujours nul.

Pendant ce temps, j'ai d'autres colonnes qui stockent des valeurs de base de date-heure de décalage inconnu (fournies par les utilisateurs), donc elles sont simplement stockées / affichées "telles quelles", et non comparées à quoi que ce soit.

Par conséquent, j'ai besoin d'une solution que je puisse appliquer à des colonnes spécifiques.

Définissez une méthode d'extension 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));
}

Cela peut ensuite être utilisé sur les propriétés dans la configuration du modèle:

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

Il a le petit avantage par rapport aux attributs que vous ne pouvez l'appliquer qu'aux propriétés du type correct.

Notez qu'il suppose que les valeurs de la base de données sont en UTC, mais qu'elles ont juste le mauvais Kind. Par conséquent, il contrôle les valeurs que vous essayez de stocker dans la base de données, lançant une exception descriptive si elles ne sont pas UTC.

Daniel Earwicker
la source
1
C'est une excellente solution qui devrait être plus élevée, surtout maintenant que la plupart des nouveaux développements utiliseront Core ou .NET 5. Points imaginaires bonus pour la politique d'application de l'UTC - si plus de personnes conservaient leurs dates UTC jusqu'à l'affichage réel de l'utilisateur, nous n'aurions pratiquement aucun bogue de date / heure.
oflahero
1

Pour ceux qui ont besoin de réaliser la solution @MattJohnson avec .net framework 4 comme moi, avec une limitation de la syntaxe / méthode de réflexion, cela nécessite une petite modification comme indiqué ci-dessous:

     foreach (var property in properties)
        {     

            DateTimeKindAttribute attr  = (DateTimeKindAttribute) Attribute.GetCustomAttribute(property, typeof(DateTimeKindAttribute));

            if (attr == null)
                continue;

            var dt = property.PropertyType == typeof(DateTime?)
                ? (DateTime?)property.GetValue(entity,null)
                : (DateTime)property.GetValue(entity, null);

            if (dt == null)
                continue;

            //If the value is not null set the appropriate DateTimeKind;
            property.SetValue(entity, DateTime.SpecifyKind(dt.Value, attr.Kind) ,null);
        }  
Sxc
la source
1

La solution de Matt Johnson-Pint fonctionne, mais si tous vos DateTimes sont censés être UTC, la création d'un attribut serait trop détournée. Voici comment je l'ai simplifié:

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

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

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

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

Une autre approche serait de créer une interface avec les propriétés datetime, de les implémenter sur les classes d'entités partielles. Et puis utilisez l'événement SavingChanges pour vérifier si l'objet est du type d'interface, définissez ces valeurs datetime sur ce que vous voulez. En fait, si ceux-ci sont créés / modifiés à des dates, vous pouvez utiliser cet événement pour les remplir.

AD.Net
la source
0

Dans mon cas, je n'avais qu'une seule table avec des dates-heures UTC. Voici ce que j'ai fait:

public partial class MyEntity
{
    protected override void OnPropertyChanged(string property)
    {
        base.OnPropertyChanged(property);            

        // ensure that values coming from database are set as UTC
        // watch out for property name changes!
        switch (property)
        {
            case "TransferDeadlineUTC":
                if (TransferDeadlineUTC.Kind == DateTimeKind.Unspecified)
                    TransferDeadlineUTC = DateTime.SpecifyKind(TransferDeadlineUTC, DateTimeKind.Utc);
                break;
            case "ProcessingDeadlineUTC":
                if (ProcessingDeadlineUTC.Kind == DateTimeKind.Unspecified)
                    ProcessingDeadlineUTC = DateTime.SpecifyKind(ProcessingDeadlineUTC, DateTimeKind.Utc);
            default:
                break;
        }
    }
}
Ronnie Overby
la source