.NET6和DateTime问题。无法将Kind=UTC的DateTime写入到PostgreSQL类型为'timestamp without time zone'中。

83

我有一个常见的问题。

无法将带有Kind=UTC的DateTime写入到PostgreSQL类型为'timestamp without time zone'中

我想启用遗留时间戳行为,如此文档所述: https://github.com/npgsql/doc/blob/main/conceptual/Npgsql/types/datetime.md/

public MyDbContext(DbContextOptions<MyDbContext> contextOptions) : base(contextOptions)
        {
            AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
            AppContext.SetSwitch("Npgsql.DisableDateTimeInfinityConversions", true);
        }

但是不起作用。我仍然得到同样的错误。

我做错了什么?为什么旧行为不起作用?


你需要分享一些代码来展示你正在做什么,以及包含堆栈跟踪的完整异常信息。 - Shay Rojansky
2
重大更改 https://github.com/npgsql/efcore.pg/issues/2000 - Nick Kovalsky
1
@NickKovalsky,“重大变更”提到“'无法将Kind = Unspecified的DateTime写入PostgreSQL类型'timestamp with time zone'”,这是有道理的,而当前的错误则提到了Kind = UTC。在我看来,这个问题中提到的错误消息非常令人困惑,因为它暗示日期已经是Kind.UTC。当我使用Kind.UTC传递SaveChanges时,我也遇到了这个错误,这是没有意义的(我希望它能够将UTC日期持久化到没有时区的时间戳中)。 - Alexei - check Codidact
我觉得有必要对“为什么”以这种方式实现这种行为进行评论 - 这是因为Postgresql实际上甚至不在“带时区的时间戳”类型中存储时区 - 它只是假设输入时间是在UTC中。所以在这个问题上“开枪打脚”真的很容易,尤其是当你使用类似DateTime.Now这样的东西时。关于这个问题的更多信息可以在这里找到 - https://stackoverflow.com/questions/5876218/difference-between-timestamps-with-without-time-zone-in-postgresql - undefined
14个回答

125

A. 通过在Startup Configure方法中添加AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);进行解决。


B. 或者如果您根本没有Startup类,而是将所有初始化都放在带有主机构建器的Program.cs中,则文件结尾可能如下所示:

... //adding services etc
var host = builder.Build();
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
... //your other scoped code
await host.RunAsync();

使用 System.Linq.Dynamic 查询数据库时,我们需要指定时间的类型。
过滤器示例: $"User.BirthDate>={time.ToStringUtc()}"

public static string ToStringUtc(this DateTime time)
{
    return $"DateTime({time.Ticks}, DateTimeKind.Utc)";
}

同时,@istvan-kardkovacs的答案https://dev59.com/2lEG5IYBdhLWcg3wOnWy#70142836同样适用。基本上,对于每个你正在创建的= new DateTime(),添加一个.SetKindUtc()。上面的切换在我的后台托管服务中显然无法正常工作,在执行任何其他代码之前,该服务正在填充数据库。


6
从Core 3.1升级到.NET 6后,我遇到了这个错误,以下是最简单的解决方法。 - Ivica Buljević
2
在使用 .net6 应用程序和 PostgreSQL 数据库时遇到了这个问题。我在启动项中添加了选项 B 并解决了它。 - Rob
如果您正在使用NHibernate,我建议使用低于版本6的Npgsql(因为NHibernate尚未实现正确的映射=> https://github.com/nhibernate/nhibernate-core/issues/2994) - nulldevops

28

在创建、插入、更新操作中,以及 Linq 查询中的 DateTime 比较中,您必须为所有 DateTime 字段设置 DateTimeKind。我已经创建了一个小扩展方法,并将其添加到所有日期字段中。

public static class DateTimeExtensions
{
    public static DateTime? SetKindUtc(this DateTime? dateTime)
    {
        if (dateTime.HasValue)
        {
            return dateTime.Value.SetKindUtc();
        }
        else
        {
            return null;
        }
    }
    public static DateTime SetKindUtc(this DateTime dateTime)
    {
        if (dateTime.Kind == DateTimeKind.Utc) { return dateTime; }
        return DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);
    }
}

还需要编写单元测试来展示功能:

using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MyNamespace;

[TestClass]
[ExcludeFromCodeCoverage]
public class DateTimeExtensionsTests
{
    [TestMethod]
    public void SetKindUtcNullInputTest()
    {
        DateTime? input = null;
        DateTime? result = input.SetKindUtc();
        Assert.IsNull(result);
    }

    [TestMethod]
    public void SetKindUtcNonNullRegularDateInputTest()
    {
        DateTime? input = DateTime.Now;
        DateTime? result = input.SetKindUtc();
        Assert.IsNotNull(result);
        /* below is the primary functionality.  if the input did not have a "Kind" set, it gets set to DateTimeKind.Utc */
        Assert.AreEqual(DateTimeKind.Utc, result.Value.Kind);
    }

    [TestMethod]
    public void SetKindUtcNonNullOffsetDateInputTest()
    {
        DateTime? input = DateTime.Now;
        DateTime withKindUtcInput = DateTime.SpecifyKind(input.Value, DateTimeKind.Utc);
        DateTime? result = withKindUtcInput.SetKindUtc();
        Assert.IsNotNull(result);
        /* Utc "in" remains "Utc" out */
        Assert.AreEqual(DateTimeKind.Utc, result.Value.Kind);
    }
    
    [TestMethod]
    public void UnspecifiedKindIsOverwrittenTest()
    {
        DateTime? input = DateTime.Now;
        DateTime withKindUtcInput = DateTime.SpecifyKind(input.Value, DateTimeKind.Unspecified);
        DateTime? result = withKindUtcInput.SetKindUtc();
        Assert.IsNotNull(result);
        /* note the behavior.  "DateTimeKind.Unspecified" with overwritten with DateTimeKind.Utc */
        Assert.AreEqual(DateTimeKind.Utc, result.Value.Kind);
    }
    
    [TestMethod]
    public void LocalKindIsOverwrittenTest()
    {
        DateTime? input = DateTime.Now;
        DateTime withKindUtcInput = DateTime.SpecifyKind(input.Value, DateTimeKind.Local);
        DateTime? result = withKindUtcInput.SetKindUtc();
        Assert.IsNotNull(result);
        /* note the behavior.  "DateTimeKind.Local" with overwritten with DateTimeKind.Utc */
        Assert.AreEqual(DateTimeKind.Utc, result.Value.Kind);
    }    
}

19

将设置放在数据库上下文的静态构造函数中是一个好的位置。

在这种情况下,启动类保持更清晰。
如果您有多个项目使用相同的数据库上下文,这也是很有用的。
例如:

public class MyContext : DbContext
{
    static MyContext()
    {
        AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
    }
    
    // Other stuff of your context
}

2023年7月更新 - 解决方案#2

今天我在一个项目中遇到了一个问题,这个技巧在其中一个项目中不起作用。
经过调查研究,我找到了另一种方法来确保在任何其他代码运行之前可以设置标志。

它是通过ModuleInitializer属性来完成的:

注意:它需要C#9及更高版本(.NET 5+

只需将一个新文件添加到包含DbContext的项目中。
然后将以下内容放入文件中:

using System.Runtime.CompilerServices;

namespace Your.Project.Namespace;

public static class MyModuleInitializer
{
    [ModuleInitializer]
    public static void Initialize()
    {
        AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
    }
}

我们尝试了这个,但仍然遇到了与下面相同的异常。 无法将Kind=Local的DateTime写入PostgreSQL类型为'timestamp with time zone'的字段中,特别是在数组/范围中的上下文种类。 - Dreamer
@Dreamer,我更新了一个答案,提供了第二个解决方案,希望它能像对我有帮助一样对你有帮助。 - Anton Palyok
2023年7月的更新解决了我的问题。看起来第一个解决方案已经失效,因为我已经实施了它。 - undefined

14

Nick已经回答了这个问题,我只是想为这个时区问题增加另一种解决方案。

你可以不启用那个选项,而是使用这个扩展将所有日期时间转换后再写入。这就是我所做的。

创建这个扩展类:

public static class UtcDateAnnotation
{
    private const string IsUtcAnnotation = "IsUtc";
    private static readonly ValueConverter<DateTime, DateTime> UtcConverter = new ValueConverter<DateTime, DateTime>(convertTo => DateTime.SpecifyKind(convertTo, DateTimeKind.Utc), convertFrom => convertFrom);

    public static PropertyBuilder<TProperty> IsUtc<TProperty>(this PropertyBuilder<TProperty> builder, bool isUtc = true) => builder.HasAnnotation(IsUtcAnnotation, isUtc);

    public static bool IsUtc(this IMutableProperty property)
    {
        if (property != null && property.PropertyInfo != null)
        {
            var attribute = property.PropertyInfo.GetCustomAttribute<IsUtcAttribute>();
            if (attribute is not null && attribute.IsUtc)
            {
                return true;
            }

            return ((bool?)property.FindAnnotation(IsUtcAnnotation)?.Value) ?? true;
        }
        return true;
    }

    /// <summary>
    /// Make sure this is called after configuring all your entities.
    /// </summary>
    public static void ApplyUtcDateTimeConverter(this ModelBuilder builder)
    {
        foreach (var entityType in builder.Model.GetEntityTypes())
        {
            foreach (var property in entityType.GetProperties())
            {
                if (!property.IsUtc())
                {
                    continue;
                }

                if (property.ClrType == typeof(DateTime) ||
                    property.ClrType == typeof(DateTime?))
                {
                    property.SetValueConverter(UtcConverter);
                }
            }
        }
    }
}
public class IsUtcAttribute : Attribute
{
    public IsUtcAttribute(bool isUtc = true) => this.IsUtc = isUtc;
    public bool IsUtc { get; }
}

并将该转换器添加到您的DbContext文件中:

protected override void OnModelCreating(ModelBuilder builder)
{
     builder.ApplyUtcDateTimeConverter();//Put before seed data and after model creation
}

在写入数据库之前,这将把所有DateTime和DateTime?对象转换为Utc日期。

这将成为支持此PostgreSql数据库的唯一途径,因为我需要支持一些数据库(Sql Server、PostgreSql和很快的MySql)。手动将每个datetime值转换为Utc不是一个好的解决方案。

我们的应用程序目前还没有时区要求,但使用该扩展程序,我们可以很容易地添加时区支持。


2
它能够正常工作!我认为这是遗留项目最好的解决方案。 - Admdebian
这是一个非常优雅的解决方案。我正在研究将同样的原则应用到我的旧 System.DateTime 映射中,考虑使用 NHibernate。对于处理日期、时间和时刻类型,我在思考是否要遵循推荐的 Nodatime,并使用 npgsql 来处理新实体。 - Mathieu DSTP

11

在我的情况下,这是我犯的一个错误

InvitedOn = DateTime.Now

应该已经完成。
InvitedOn = DateTime.UtcNow

它奏效了。


注释中有-1。有人能解释一下为什么吗?这是否是一种不好的方法,还是仅仅因为对于已经存在的/传统程序来说不切实际? - JuztBe
可能是因为它没有解释任何内容,而且有点脱离上下文?通常最好使用UtcNow(简短的答案),可以消除所有时区问题。 但我遇到了相同的错误,而这个方法解决了它。希望它能帮助其他人。 - Daniel Pace
1
@DanielPace 为什么你认为使用 UtcNow 是一个“更好的选择”?考虑到大量本地应用程序存储本地日期时间可能比使用 UTC 并且一直进行转换更可取。是因为某位微软员工说它更好吗?还是因为库维护者 Shay R. 说了?由于 EF 提供了一个抽象层,我们不应该将 Postgres 目标与 MS-SQL 目标区别对待。多年来,使用 DateTimeOffset 会自动转换为 UTC,但突然间所有东西都出问题了,当针对 Postgres 时,我们不得不在应用程序代码中分支出 DateTimeOffset 的赋值?! - timmi4sa
我认为问题在于Postgres不处理时区,所以EF可能无法为您抽象出这一部分(缺少数据)。但是除了这个问题之外,您应该使用UTC,因为它在许多方面和场景中更简单,我无法在此评论框中解释。我也不是最适合解释的人,我相信其他很多人已经解释过了。 但是为了简单起见,您的用户可能会跨越不同的时区旅行,UI/应用程序应该处理这一点。您可能还希望查询数据库并根据时间进行筛选,MSSQL可能可以处理(我不确定),但其他数据库可能不能处理。 - Daniel Pace

6

我在我的DbContext中添加了代码,以设置所有模型上的日期属性:

//dbcontext
public override int SaveChanges()
{
    _changeTrackerManager?.FixupEntities(this);
    return base.SaveChanges();
}

//don't forget the async method!
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
    _changeTrackerManager?.FixupEntities(this);
    return base.SaveChangesAsync();
}

将会注入这个IChangeTrackerManager依赖项,然后任何时候保存实体时,它都会调用下面的方法,以修复所有的 UTC 日期时间类型。

public void FixupEntities(DbContext context)
{
    var dateProperties = context.Model.GetEntityTypes()
        .SelectMany(t => t.GetProperties())
        .Where(p => p.ClrType == typeof(DateTime))
        .Select(z => new
        {
            ParentName = z.DeclaringEntityType.Name,
            PropertyName = z.Name
        });

    var editedEntitiesInTheDbContextGraph = context.ChangeTracker.Entries()
        .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified)
        .Select(x => x.Entity);

    foreach (var entity in editedEntitiesInTheDbContextGraph)
    {
        var entityFields = dateProperties.Where(d => d.ParentName == entity.GetType().FullName);

        foreach (var property in entityFields)
        {
            var prop = entity.GetType().GetProperty(property.PropertyName);

            if (prop == null)
                continue;

            var originalValue = prop.GetValue(entity) as DateTime?;
            if (originalValue == null)
                continue;

            prop.SetValue(entity, DateTime.SpecifyKind(originalValue.Value, DateTimeKind.Utc));
        }
    }
}

5
也许有点晚了,但对我来说我刚刚创建了这个转换器。
public class DateTimeToDateTimeUtc : ValueConverter<DateTime, DateTime>
{
    public DateTimeToDateTimeUtc() : base(c => DateTime.SpecifyKind(c, DateTimeKind.Utc), c => c)
    {

    }
}
 protected sealed override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
    {
        configurationBuilder.Properties<DateTime>()
            .HaveConversion(typeof(DateTimeToDateTimeUtc));
    }

3

我用了这种方法,但是当我在PostgreSQL中存储我的日期时,带有时区。 - P_R

3

在尝试将Kind = UTC的DateTime(实际上我正在使用DateTime.UtcNow)持久化到“没有时区的时间戳”列时,我也遇到了这个错误。在我看来,这个错误是没有意义的,因为UTC日期时间应该是正确的。

我的解决方法是切换到“带有时区的时间戳”,因为:

  • 它似乎按预期工作:我得到{UTC时间戳} + 00
  • “EnableLegacyTimestampBehavior”听起来像是未来可能会被弃用的东西
  • 将所有DateTime转换为UTC Kind虽然优雅,但听起来像是一种可能在某些场景下会产生负面影响的hack,而我真正想要其他类型的DateTime

2

稍微修改了 @DLeh 的答案 https://dev59.com/2lEG5IYBdhLWcg3wOnWy#71179214

    private void ConvertDateTimesToUniversalTime()
    {
        var modifiedEntites = ChangeTracker.Entries<IHaveAggregateRootId>()
                .Where(e => (e.State == EntityState.Added || e.State == EntityState.Modified || e.State == EntityState.Deleted)).ToList();
        foreach (var entry in modifiedEntites)
        {
            foreach (var prop in entry.Properties)
            {
                if (prop.Metadata.ClrType == typeof(DateTime))
                {
                    prop.Metadata.FieldInfo.SetValue(entry.Entity, DateTime.SpecifyKind((DateTime)prop.CurrentValue, DateTimeKind.Utc));
                }
                else if (prop.Metadata.ClrType == typeof(DateTime?) && prop.CurrentValue != null)
                {
                    prop.Metadata.FieldInfo.SetValue(entry.Entity, DateTime.SpecifyKind(((DateTime?)prop.CurrentValue).Value, DateTimeKind.Utc));
                }
            }
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接