EF Core 的 JSON 时长超过一天,投影和筛选却读错值
SQL Server JSON 中的 TimeSpan 可以完整保存,但把字段投影成 time 会丢失或误读天数。先比对原始 JSON 与生成 SQL,临时读取完整实体,再决定是否迁移时长存储。
同一条记录,查询完整实体时 TimeSpan 是 26 小时,只投影 JSON 里的时长属性却变成另一段时间;再加一个“小于 2 小时”的条件,这条记录甚至被错误地筛选出来。
在 SQL Server 的 EF Core JSON 映射里,先把原始 JSON 和生成 SQL 放在一起看。如果 JSON 中保留了天数,而 SQL 对 JSON_VALUE 的结果使用 CAST(... AS time),不能继续把它当作普通精度问题。
时长和 SQL time 的范围不同
SQL Server 的 time 表示一天内的时间。带天数的 TimeSpan 是持续时长,26 小时不等于第二天的一点钟,也不能通过补一个日期来维持原来的时长比较语义。
在 EF Core SQL Server 10.0.12、10.0.14 环境中,OwnsOne(...).ToJson() 和复杂属性 JSON 映射都出现了这一差别:完整对象的 JSON 反序列化保留时长,属性投影和筛选则走 SQL 标量转换。检查时必须覆盖应用实际使用的查询形状,而不只是“写入后读完整实体成功”。
可用 ToQueryString() 查看转换,但不要把带敏感参数的完整 SQL 直接写入公开日志。若应用另有自定义转换器、JSON 序列化选项或列类型,先确认它们是否改变了实际表示。
临时绕过错误的标量转换
对于按主键或其他可靠条件限定的少量记录,可以先读取完整实体,再在内存中使用其 JSON 对象的 TimeSpan。下面假定 Item.Document.Length 已按项目模型映射:
var item = await context.Items
.AsNoTracking()
.SingleAsync(item => item.Id == id);
TimeSpan length = item.Document.Length;
bool isShort = length < TimeSpan.FromHours(2);
先用已知超过一天的记录验证完整实体读取路径,再采用这个入口。原来按时长执行的数据库条件必须移出受影响的 SQL;如果条件仍先在数据库中排除了记录,后面的客户端比较无法把它找回来。
这条路径适合已经严格限定大小的结果集。不要直接在整张表上插入 AsEnumerable(),把所有 JSON 拉进应用。分页、计数和排序若依赖时长,在客户端计算后会改变语义和成本,需要单独设计,不能照搬这个按主键的示例。
需要服务器筛选时,重新明确存储契约
若业务必须经常按任意时长筛选,可评估把时长保存为具有足够范围的整数或数值,例如约定单位的 ticks 或微秒。要同时规定精度、允许范围、负值和 NULL 的含义,并同步读写两端。只修改 CLR 类型或列名,不会自动把现有 JSON 值转换成正确单位。
迁移前保留原 JSON 和可恢复备份,在副本上核对零、一天以内、正好一天、多天、负时长和最大业务值。只有旧值与新表示逐项对应、筛选结果和排序一致,才进入正式迁移。
TimeSpan 字面量转换与 JSON 字段提取走不同路径。升级后仍需比较完整读取、标量投影和数据库筛选这三条路径。对于已经受到错误筛选影响的业务结果,还要重新核对记录范围,不能只验证新查询不报错。


