Est-il possible que Entity Framework (j'utilise l'approche Code First avec CTP5 actuellement) stocke toutes les valeurs DateTime en UTC dans la base de données?
Ou existe-t-il 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");
Voici une approche que vous pourriez envisager:
Définissez d'abord 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));
}
}
}
Accrochez maintenant 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 quel DateTime
ou DateTime?
propriétés, 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éfinira le DateTimeKind
que vous spécifiez, comme 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 type lors de la récupération, ce qui lui permet d'être sérialisé en UTC ou converti en d'autres fuseaux horaires avec TimeZoneInfo
.
J'aime vraiment l'approche de Matt Johnson, mais dans mon modèle, TOUS mes membres DateTime sont en UTC et je ne veux pas avoir à tous les décorer 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 à ceci:
/// <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));
}
}
}
}
Cette réponse fonctionne avec Entity Framework 6
La réponse acceptée ne fonctionne pas pour les objets projetés ou anonymes. Les performances pourraient également être un problème.
Pour ce faire, 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 pour sur votre DbContext
[DbConfigurationType(typeof(MyDbConfiguration ))]
internal class MyDbContext : DbContext
{
// ...
}
C'est ça. À votre santé.
Pour simplifier, voici l'implémentation complète 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);
}
}
}
Je crois avoir trouvé une solution qui ne nécessite aucune vérification UTC personnalisée ni manipulation DateTime.
Fondamentalement, vous devez modifier vos entités EF pour utiliser le type de données DateTimeOffset (NOT 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 lorsqu'il est retransmis à mon serveur, il est à nouveau automatiquement converti en UTC, également comme prévu.
Je fais des recherches là-dessus en ce moment, et la plupart de ces réponses ne sont pas vraiment bonnes. D'après ce que je peux voir, il n'y a aucun moyen de dire à EF6 que les dates de sortie de la base de données sont au format UTC. Si tel est le cas, le moyen le plus simple de vous 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 comme 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 contient 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 en UTC dans la base de données, la dernière branche fonctionnera très bien pour vous.
DateTime.Now
est toujours DateTimeKind.Local
, donc l'algorithme ci-dessus fonctionne très 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 DateTimeKind.Unspecified
peut se faufiler dans votre code. Par exemple, vous pouvez désérialiser vos modèles à partir des données JSON, et votre saveur de désérialiseur par défaut est de ce type. A vous de vous prémunir contre les dates localisées marquées DateTimeKind.Unknown
d'accéder à ce setter de n'importe qui sauf EF.
Si vous prenez soin de bien transmettre les dates UTC lorsque vous définissez les valeurs et que tout ce qui vous intéresse est de vous assurer que DateTimeKind est correctement défini lorsque les entités sont récupérées de la base de données, consultez ma réponse ici: https: // stackoverflow.com/a/9386364/27959
Il n'existe aucun moyen de spécifier le DataTimeKind dans Entity Framework. Vous pouvez décider de convertir les valeurs de date-heure en utc avant de les stocker dans 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 en utilisant l'objet DateTimeOffset au lieu de DateTime.
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);
}
Dans mon cas, je n'avais qu'une seule table avec des 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;
}
}
}
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 interface, définissez ces valeurs datetime sur ce que vous voulez. En fait, si ceux-ci sont créés à des dates différentes, vous pouvez utiliser cet événement pour les remplir.