DEBUG.WIKI
丽萨主机;为跨境业务选好 IP;原生 / 住宅 IP · 按套餐选择;查看 IP →
阿里云;从建站到云上业务;云服务器 · 数据库 · 对象存储;查看产品 →
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

Npgsql 列是 timestamp without time zone,却报只接受 UTC:检查参数类型

Npgsql 写入 DateTime 报 timestamp 类型不匹配时,先检查参数推断、目标列类型和 DateTime.Kind,再决定是否转换为 UTC。

升级到 Npgsql 6 后,写入 timestamp without time zone 列,却报 Cannot write DateTime with Kind=Unspecified to PostgreSQL type 'timestamp with time zone', only UTC is supported。先检查发送参数的类型:报错里的 PostgreSQL 类型可能来自参数配置,不是驱动查到了目标列的类型。

Npgsql 6 调整了时间类型映射。尤其是显式设置 DbType.DateTime 的旧代码,它现在对应 timestamptz;即使数据库列仍是 timestamp without time zone,发送之前也会因非 UTC 值与参数类型不匹配而失败。

同时看列类型、参数类型和 DateTime.Kind

对于失败的那条命令,检查参数的 DbType、NpgsqlDbType、值的 CLR 类型与 DateTime.Kind。不要只看表结构截图。以下是参数检查的结构示例,假设 command 已创建且没有在输出中记录值:

foreach (Npgsql.NpgsqlParameter parameter in command.Parameters)
{
    Console.WriteLine($"{parameter.ParameterName}: {parameter.DbType}, {parameter.NpgsqlDbType}");
    if (parameter.Value is DateTime value)
        Console.WriteLine($"DateTime.Kind: {value.Kind}");
}

用于定位的日志只需类型与 Kind,不必打印真实日期或整条含业务数据的命令。再核对这条连接实际访问的表和列,避免在另一数据库检查表结构。

timestamp without time zone 表示没有时区信息的日期时间;timestamptz 表示一个时间点,不保存原始时区名称。应该使用哪一种,由业务数据含义决定。日期列则应采用 date 映射,不必为了消除异常把一个纯日期包装成 UTC 时间点。

保留非 UTC 日期时间时,发送 Timestamp 参数

如果业务确实要保存不带时区的日期时间,目标列也确实是 timestamp without time zone,可以明确指定参数类型。下面是命令参数片段示例,command 与其 SQL 中的 @created_at 已由项目创建:

var value = new DateTime(2026, 10, 5, 9, 30, 0, DateTimeKind.Unspecified);
command.Parameters.Add("created_at", NpgsqlTypes.NpgsqlDbType.Timestamp).Value = value;

示例日期仅用于说明类型,不是业务默认值。显式指定 Timestamp,使值与发送类型配合。对于使用通用 DbType 的代码,Npgsql 6 的 DbType.DateTime2 对应这种 timestamp 类型;它的映射含义不能简单照搬 SQL Server 的同名类型。

如果 ORM 负责生成参数,应改它的属性映射,不能在一段独立命令里设置类型后,认为 ORM 也改了。EF Core PostgreSQL 提供程序 6 默认把 DateTime 属性映射到 timestamptz;保留非 UTC 语义时,应按模型明确配置 timestamp without time zone。数据库列、模型快照与实际生成参数仍要逐项对应。

保存时间点时,要取得真正的 UTC 值

如果数据代表真实时间点,应在输入边界明确原始时区,转换为 UTC,再配合 TimestampTz 或相应模型映射。DateTime.SpecifyKind(value, DateTimeKind.Utc) 只改变标记,不做时区转换;不能用它把不明来源的“上午九点半”直接变成同一时刻的 UTC。

数组或范围还需核对每个元素的时间语义与 Kind,不能只改一个元素。错误文字提到数组,不代表本次一定使用了数组;单个不匹配参数也会出现相关说明。

修改表类型属于数据迁移,不能仅为让发送参数通过就执行。把已有 timestamp 转为 timestamptz 时,PostgreSQL 会依据转换时的时区解释原数据;错误的假设可能改变全部历史时间点。需要先确认历史数据的含义、保留备份、审查迁移与回退,再安排部署。

Npgsql.EnableLegacyTimestampBehavior 是兼容开关,会影响时间读写行为。它不能代替对业务时间的确认,也不宜作为永久修复到处开启。

处理后,在隔离的数据副本或获准的验证范围内核对发送类型、写入和读回结果,确认日期时间值及 Kind 符合业务预期;没有异常只是其中一项。若映射变更影响其他读取,恢复保留的应用配置与版本;已执行的数据迁移需按迁移恢复方案处理,不能只切回客户端包。