ChatGPT解决这个技术问题 Extra ChatGPT

实体框架日期时间和 UTC

是否可以让实体框架(我目前使用 CTP5 的代码优先方法)将所有 DateTime 值作为 UTC 存储在数据库中?

或者是否有一种方法可以在映射中指定它,例如在 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");

M
Matt Johnson-Pint

这是您可能会考虑的一种方法:

首先,定义以下属性:

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

现在将该属性与您的 EF 上下文挂钩:

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

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

现在,您可以在任何 DateTimeDateTime? 属性上应用此属性:

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

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

有了这一点,每当 Entity Framework 从数据库加载实体时,它都会设置您指定的 DateTimeKind,例如 UTC。

请注意,保存时这不会做任何事情。在尝试保存之前,您仍然必须将值正确转换为 UTC。但它确实允许您在检索时设置种类,这允许将其序列化为 UTC,或使用 TimeZoneInfo 转换为其他时区。


如果你不能让这个工作,你可能错过了这些用法之一: using System;使用 System.Collections.Generic;使用 System.ComponentModel.DataAnnotations.Schema;使用 System.Linq;使用 System.Reflection;
@Saustrup - 你会发现大多数关于 SO 的例子都会为了简洁而省略使用,除非它们与问题直接相关。不过谢谢。
@MattJohnson 没有 @Saustrup 的 using 语句,你会得到一些无用的编译错误,例如 'System.Array' does not contain a definition for 'Where'
正如@SilverSideDown 所说,这只适用于.NET 4.5。我在 gist.github.com/munr/3544bd7fab6615290561 创建了一些扩展以使其与 .NET 4.0 兼容。需要注意的另一件事是,这不适用于投影,仅适用于完全加载的实体。
有什么建议可以通过预测来实现吗?
s
ssmith

对于 EF Core,GitHub 上有关于此主题的精彩讨论:https://github.com/dotnet/efcore/issues/4711

一种解决方案(归功于 Christopher Haws)将导致在将所有日期存储到数据库/从数据库中检索它们时将它们视为 UTC,将以下内容添加到 DbContext 类的 OnModelCreating 方法中:

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.IsKeyless)
    {
        continue;
    }

    foreach (var property in entityType.GetProperties())
    {
        if (property.ClrType == typeof(DateTime))
        {
            property.SetValueConverter(dateTimeConverter);
        }
        else if (property.ClrType == typeof(DateTime?))
        {
            property.SetValueConverter(nullableDateTimeConverter);
        }
    }
}

此外,如果您想排除某些实体的某些属性不被视为 UTC,请使用 check this link


对我来说绝对是最好的解决方案!谢谢
@MarkRedman我认为这没有意义,因为如果您有合法的 DateTimeOffset 用例,您还想保留有关时区的信息。有关何时在 DateTime 和 DateTimeOffset 之间进行选择,请参阅 docs.microsoft.com/en-us/dotnet/standard/datetime/…stackoverflow.com/a/14268167/3979621
IsQueryType 似乎已被 IsKeyLess 取代:github.com/dotnet/efcore/commit/…
为什么需要 IsQueryType(或现在的 IsKeyLess)检查?
实际上,它可能不需要。根据 EF 核心的特定配置,如果他们决定不省略无密钥实体类型,可能会或可能不会遇到问题。
B
Bob.at.Indigo.Health

我真的很喜欢 Matt Johnson 的方法,但在我的模型中,我的所有 DateTime 成员都是 UTC,我不想用一个属性来装饰它们。所以我概括了 Matt 的方法,以允许事件处理程序应用默认的 Kind 值,除非成员显式地使用该属性进行修饰。

ApplicationDbContext 类的构造函数包括以下代码:

/// <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 如下所示:

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

这是对已接受答案的非常有用的扩展!
也许我遗漏了一些东西,但是这如何默认为 DateTimeKind.Utc 而不是 DateTimeKind.Unspecified?
@Rhonage 对此感到抱歉。默认设置在 ApplicationDbContext 构造函数中。我更新了答案以包含它。
@Bob.at.AIPsychLab 谢谢伙计,现在清楚多了。试图弄清楚是否有一些重量反射正在进行 - 但不,非常简单!
如果模型具有没有(公共)setter 方法的 DateTIme 属性,则会失败。建议编辑。另请参阅stackoverflow.com/a/3762475/2279059
J
Joel Bourbonnais

此答案适用于实体框架 6

接受的答案不适用于 Projected 或 Anonymous 对象。性能也可能是一个问题。

为此,我们需要使用 EntityFramework 提供的对象 DbCommandInterceptor

创建拦截器:

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,我们将其替换为我们的

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

在您的 DbConfiguration 中注册拦截器

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

最后,在您的 DbContext 上注册配置

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

而已。干杯。

为简单起见,这里是 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);
        }
    }
}

到目前为止,这似乎是最好的答案。我首先尝试了属性变化,因为它似乎影响不大,但是我的单元测试会因模拟而失败,因为构造函数事件绑定似乎不知道 OnModelCreating 事件中发生的表映射。这个得到我的投票!
为什么要跟踪 DisposeGetData
这段代码应该归功于@IvanStoev:stackoverflow.com/a/40349051/90287
不幸的是,如果您正在映射空间数据,这将失败
@user247702 是的,阴影 Dispose 是错误的,覆盖 Dispose(bool)
D
Daniel Earwicker

又一年,另一种解决方案!这适用于 EF Core。

我有很多映射到 DateTimeDATETIME2(7) 列,并且始终存储 UTC。我不想存储偏移量,因为如果我的代码正确,那么偏移量将始终为零。

同时,我还有其他列存储未知偏移量的基本日期时间值(由用户提供),因此它们只是“按原样”存储/显示,而不是与任何东西进行比较。

因此,我需要一个可以应用于特定列的解决方案。

定义扩展方法 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));
}

然后可以将其用于模型设置中的属性:

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

它与属性相比具有较小的优势,您只能将其应用于正确类型的属性。

请注意,它假定数据库中的值采用 UTC 格式,但 Kind 错误。因此,它会管理您尝试存储在数据库中的值,如果它们不是 UTC,则会引发描述性异常。


这是一个很好的解决方案,尤其是现在大多数新开发将使用 Core 或 .NET 5。对于 UTC 执行策略的额外想象点 - 如果更多人将他们的日期一直保持 UTC 到实际用户显示,我们几乎没有任何日期/时间错误。
我喜欢解决方案的清洁度,但在“定义扩展方法”步骤中迷失了......究竟在哪里/如何?
M
Moutono

我相信我找到了一个不需要任何自定义 UTC 检查或 DateTime 操作的解决方案。

基本上,您需要更改 EF 实体以使用 DateTimeOffset (NOT DateTime) 数据类型。这将在数据库中存储带有日期值的时区(在我的例子中是 SQL Server 2015)。

当 EF Core 从数据库请求数据时,它也会收到时区信息。当您将此数据传递给 Web 应用程序(在我的情况下为 Angular2)时,日期会自动转换为浏览器的本地时区,这正是我所期望的。

当它被传回我的服务器时,它会再次自动转换为 UTC,也符合预期。


DateTimeOffset 不存储时区,这与通常的看法相反。它存储该值表示的与 UTC 的偏移量。偏移量不能反向映射以确定创建偏移量的实际时区,从而使数据类型几乎无用。
否,但它可用于正确存储 DateTime:medium.com/@ojb500/in-praise-of-datetimeoffset-e0711f991cba
只有 UTC 不需要位置,因为它无处不在。如果您使用 UTC 以外的其他东西,您还需要位置,否则时间信息是无用的,在使用 datetimeoffset 时也是如此。
DATETIMEOFFSET 将做原始发布者想要的:将日期时间存储为 UTC,而无需执行任何(显式)转换。 @Carl DATETIME、DATETIME2 和 DATETIMEOFFSET 都正确存储日期时间值。除了额外存储与 UTC 的偏移量之外,DATETIMEOFFSET 几乎没有任何优势。您在数据库中使用的是您的电话。我只是想强调一点,它不会像许多人错误地认为的那样存储时区。
@Suncat2000 优点是您可以按原样将此日期从您的 api 发送到您的客户端浏览器。当客户端浏览器打开这个日期时,它知道 UCT 的偏移量是多少,因此能够将其转换为客户端正在查看它的系统上的默认日期。因此,从您的服务器时区到浏览器时区的转换无需开发人员为其编写任何代码。
s
staa99

我现在正在研究这个,这些答案中的大多数都不是很好。据我所知,没有办法告诉 EF6 从数据库中出来的日期是 UTC 格式的。如果是这种情况,确保模型的 DateTime 属性采用 UTC 的最简单方法是在 setter 中进行验证和转换。

这是一些描述算法的类似 c# 的伪代码

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

前两个分支很明显。最后一个是秘制酱汁。

当 EF6 从从数据库加载的数据创建模型时,DateTimes 为 DateTimeKind.Unspecified。如果您知道数据库中的日期都是 UTC,那么最后一个分支将非常适合您。

DateTime.Now 始终为 DateTimeKind.Local,因此上述算法适用于代码中生成的日期。大多数时候。

但是,您必须小心,因为 DateTimeKind.Unspecified 可以通过其他方式潜入您的代码。例如,您可以从 JSON 数据反序列化您的模型,并且您的反序列化程序默认为这种类型。由您负责防止标记为 DateTimeKind.Unspecified 的本地化日期从除 EF 之外的任何人那里获得该设置器。


正如我在与这个问题多年的斗争后发现的那样,如果您将 DateTime 字段分配或选择到其他结构中,例如数据传输对象,EF 会忽略 getter 和 setter 方法。在这些情况下,您仍然需要在生成结果后将 Kind 更改为 DateTimeKind.Utc。示例:from o in myContext.Records select new DTO() { BrokenTimestamp = o.BbTimestamp }; 将所有种类设置为 DateTimeKind.Unspecified
我已经将 DateTimeOffset 与 Entity Framework 一起使用了一段时间,如果您使用 DateTimeOffset 数据类型指定您的 EF 实体,那么您的所有 EF 查询都将返回带有 UTC 偏移量的日期,就像它保存在数据库中一样。因此,如果您将数据类型更改为 DateTimeOffset 而不是 DateTime,则不需要上述解决方法。
很高兴知道!谢谢@Moutono
根据@Suncat2000 评论,这根本行不通,应该被删除
V
Vijay

无法在实体框架中指定 DataTimeKind。您可能决定在存储到 db 之前将日期时间值转换为 utc,并始终假定从 db 检索的数据为 UTC。但是在查询期间具体化的 DateTime 对象将始终是“未指定”。您还可以使用 DateTimeOffset 对象而不是 DateTime 进行评估。


C
Community

如果您在设置值时小心地正确传递 UTC 日期,并且您所关心的只是确保在从数据库中检索实体时正确设置 DateTimeKind,请在此处查看我的答案:https://stackoverflow.com/a/9386364/279590


O
Ogglas

感谢@ajcvickers。从 EF Core 2.1 开始,这将是处理 DateTime.Kind 的一种方法:

modelBuilder
    .Entity<Foo>()
    .Property(e => e.SomeDate)
    .HasConversion(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc));

这将确保每次从数据库中读取日期时都会自动将其指定为 Utc。

资源:

https://github.com/dotnet/efcore/issues/4711#issuecomment-358695190


M
Mielipuoli

Matt Johnson-Pint 的解决方案有效,但如果您的所有 DateTimes 都应该是 UTC,那么创建属性将过于迂回。这是我简化它的方法:

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

s
statler

这里的解决方案很有用,但我预计很多人会遇到这样的问题,即他们希望所有日期时间都在本地时区可用,但他们希望将其翻译,以便将持久版本保存为 UTC。

实现这一点有 3 个挑战:

将数据读取为 UTC 并转换为本地调整查询参数,例如 SELECT * From PRODUCT where SALEDATE< @1 将 LocalTime 的数据存储为 UTC

1. 以UTC格式读取数据并转换为Local

在这种情况下,上述基于 Ivan Stoev DateTime.Kind set to unspecified, not UTC, upon loading from database 工作的解决方案将满足您的需求。

2.调整查询参数

与 Ivan 的拦截器解决方案类似,您可以利用 ReaderExecuting 拦截器。好处是这比 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. 将 LocalTime 的数据存储为 UTC

虽然有些查询拦截器看起来像是在这里提供帮助,但它们被多次调用并产生了意想不到的结果。我想出的最佳解决方案是覆盖 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();
    }

扩展名是(基于 Talon 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)));
        }
    }
}

S
Sxc

对于像我一样需要使用.net framework 4 实现@MattJohnson 解决方案的人,具有反射语法/方法限制,需要进行一些修改,如下所示:

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

M
Mentor

为了在 Entity Framework Core 中为实体属性定义 DateTimeKind,我建议使用以下类:

/// <summary>
/// Класс для преобразования DateTimeKind у сущностей в EF
/// </summary>
public static class DateTimeKindAnnotation
{
    private const string DateTimeKindAnnotation = "DateTimeKind";

    private static readonly ValueConverter<DateTime, DateTime> UtcConverter =
        new ValueConverter<DateTime, DateTime>(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc));

    private static readonly ValueConverter<DateTime, DateTime> LocalConverter =
        new ValueConverter<DateTime, DateTime>(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Local));

    private static readonly ValueConverter<DateTime, DateTime> UnspecifiedConverter =
        new ValueConverter<DateTime, DateTime>(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Unspecified));

    /// <summary>
    /// Данное свойство будет иметь заданный DateTimeKind в EF
    /// </summary>
    /// <typeparam name="TProperty"></typeparam>
    /// <param name="builder"></param>
    /// <param name="kind"></param>
    public static PropertyBuilder<DateTime> HasDateTimeKind(this PropertyBuilder<DateTime> builder, DateTimeKind kind) =>
        builder.HasAnnotation(DateTimeKindAnnotation, kind);

    /// <summary>
    /// Данное свойство будет иметь заданный DateTimeKind в EF
    /// </summary>
    /// <typeparam name="TProperty"></typeparam>
    /// <param name="builder"></param>
    /// <param name="kind"></param>
    public static PropertyBuilder<DateTime?> HasDateTimeKind(this PropertyBuilder<DateTime?> builder, DateTimeKind kind) =>
        builder.HasAnnotation(DateTimeKindAnnotation, kind);

    public static DateTimeKind? FindDateTimeKind(this IMutableProperty property)
    {
        var attribute = property.PropertyInfo.GetCustomAttribute<DateTimeKindAttribute>();
        if (attribute is not null)
        {
            return attribute.Kind;
        }
        return (DateTimeKind?)property.FindAnnotation(DateTimeKindAnnotation)?.Value;
    }

    /// <summary>
    /// Преобразует DateTimeKind у всех сущностей в EF к значению по умолчанию, заданному через атрибут или анотацию.
    /// </summary>
    /// <remarks>Убедитесь, что это вызывается после настройки всех ваших сущностей.</remarks>
    /// <param name="builder"></param>
    /// <param name="defaultKind">DateTimeKind, который надо использовать по умолчанию.</param>
    public static void ApplyDateTimeKindConverter(this ModelBuilder builder, DateTimeKind defaultKind = DateTimeKind.Utc)
    {
        foreach (var entityType in builder.Model.GetEntityTypes())
        {
            foreach (var property in entityType.GetProperties())
            {
                if (property.ClrType == typeof(DateTime) || property.ClrType == typeof(DateTime?))
                {
                    DateTimeKind kind = property.FindDateTimeKind() ?? defaultKind;
                    switch (kind)
                    {
                        case DateTimeKind.Utc:
                            property.SetValueConverter(UtcConverter);
                            break;
                        case DateTimeKind.Local:
                            property.SetValueConverter(LocalConverter);
                            break;
                        case DateTimeKind.Unspecified:
                            property.SetValueConverter(UnspecifiedConverter);
                            break;
                        default:
                            throw new NotSupportedException($"Kind \"{kind}\" неподдерживается");
                    }
                }
            }
        }
    }
}

/// <summary>
/// Задает тот DateTimeKind, который будет применяться EF для поля сущности.
/// </summary>
public class DateTimeKindAttribute : Attribute
{
    public DateTimeKindAttribute(DateTimeKind kind) => Kind = kind;
    public DateTimeKind Kind { get; }
}

对于其最简单的应用程序,您需要在 OnModelCreating 的末尾添加对 ApplyDateTimeKindConverter() 的调用。默认情况下,所有日期时间字段都分配有单一种类的 Utc。异常可以通过 DateTimeKindAttribute 属性或 fluentapi 方法 HasDateTimeKind (DateTimeKind.)


A
AD.Net

另一种方法是创建一个具有日期时间属性的接口,在部分实体类上实现它们。然后使用 SavingChanges 事件检查对象是否属于接口类型,将这些日期时间值设置为您想要的任何值。事实上,如果这些是创建日期/修改日期,您可以使用该事件来填充它们。


不错的主意,但这些类不会用于匿名选择。
R
Ronnie Overby

就我而言,我只有一张带有 UTC 日期时间的表。这是我所做的:

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

A
Alexey Danylov

扩展了适用于我的 EF Dotnet Core 的先前解决方案放置代码。

 public partial class XxxxxxDataContext {
    partial void CustomizeMapping(ref ModelBuilder modelBuilder) {
      var dateTimeConverter = new ValueConverter<DateTime, DateTime>(
        v => v.ToUniversalTime(),
        (v) => v.Kind == DateTimeKind.Utc?DateTime.SpecifyKind(v, DateTimeKind.Utc):TimeZoneInfo.ConvertTimeToUtc(v, TimeZoneInfo.Local));

      var nullableDateTimeConverter = new ValueConverter<DateTime?, DateTime?>(
        v => v.HasValue ? v.Value.ToUniversalTime() : v,
        v => v.HasValue ? v.Value.Kind == DateTimeKind.Utc ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : TimeZoneInfo.ConvertTimeToUtc(v.Value, TimeZoneInfo.Local) : v);

      foreach (var entityType in modelBuilder.Model.GetEntityTypes()) {
        if (entityType.IsKeyless) {
          continue;
        }

        foreach (var property in entityType.GetProperties()) {
          if (property.ClrType == typeof(DateTime)) {
            property.SetValueConverter(dateTimeConverter);
          } else if (property.ClrType == typeof(DateTime?)) {
            property.SetValueConverter(nullableDateTimeConverter);
          }
        }
      }
    }
  }