EF Core+PostgreSQL按指定时区分组DateTimeOffset字段报错求助
解决方案:EF Core 8 + PostgreSQL 按指定时区分组统计DateTimeOffset字段
问题分析
报错timestamp argument to AtTimeZone had unknown store type NULL是因为EF Core无法识别自定义AtTimeZone表达式的参数存储类型,导致Npgsql无法正确转换SQL。结合你使用的DateTimeOffset字段和NodaTime配置,推荐两种可行方案:
方案1:使用NodaTime原生集成(推荐)
已经配置了UseNodaTime(),直接利用NodaTime的时区转换能力,无需自定义表达式,类型安全且不易出错。
步骤1:注册NodaTime时区提供者
在服务配置中添加:
builder.Services.AddSingleton<IDateTimeZoneProvider>(DateTimeZoneProviders.Tzdb);
步骤2:编写查询逻辑
注入IDateTimeZoneProvider后,将DateTimeOffset转换为指定时区的日期再分组:
// 注入的时区提供者 private readonly IDateTimeZoneProvider _dateTimeZoneProvider; // 查询代码 var targetTimeZone = _dateTimeZoneProvider.GetZoneOrNull(timeZoneId); if (targetTimeZone == null) throw new ArgumentException($"无效的时区ID:{timeZoneId}"); var activityPerDay = await transactionQuery .Select(t => new { // 将DateTimeOffset转为指定时区的日期 ZoneDate = Instant.FromDateTimeOffset(t.CreatedAt).InZone(targetTimeZone).Date, t.Amount }) .GroupBy(x => x.ZoneDate) .Select(dayGroup => new ItemMetrics { DateTime = dayGroup.Key.ToDateTimeUnspecified(), // 转为DateTime,或用AtMidnight().ToDateTimeOffset()转DateTimeOffset Volume = dayGroup.Sum(x => x.Amount ?? 0) }) .ToListAsync(cancellationToken);
此方案生成的SQL会自动对应"CreatedAt" AT TIME ZONE 'xxx'逻辑,完美匹配你的需求。
方案2:修复自定义表达式的存储类型问题
如果坚持使用自定义AtTimeZone表达式,需确保表达式传递正确的存储类型映射:
步骤1:修正自定义表达式类
确保AtTimeZoneExpression继承SqlExpression并正确处理类型映射:
public class AtTimeZoneExpression : SqlExpression { public SqlExpression Operand { get; } public SqlExpression TimeZone { get; } public AtTimeZoneExpression(SqlExpression operand, SqlExpression timeZone, Type type, RelationalTypeMapping typeMapping) : base(type, typeMapping) { Operand = operand; TimeZone = timeZone; } public override SqlExpression Update(IReadOnlyList<SqlExpression> children) { Debug.Assert(children.Count == 2); return new AtTimeZoneExpression(children[0], children[1], Type, TypeMapping); } public override void Print(ExpressionPrinter printer) { printer.Visit(Operand); printer.Append(" AT TIME ZONE "); printer.Visit(TimeZone); } public override bool Equals(object? obj) => obj is AtTimeZoneExpression other && Equals(Operand, other.Operand) && Equals(TimeZone, other.TimeZone); public override int GetHashCode() => HashCode.Combine(Operand, TimeZone); }
步骤2:修正函数注册逻辑
在ModelBuilder中注册函数时,确保获取正确的类型映射:
modelBuilder.HasDbFunction(() => QueryHelper.ToTimeZoneExpression()) .HasTranslation(args => { var operand = args[0]; var timeZone = args[1]; // 从操作数获取或自动匹配类型映射 var typeMapping = operand.TypeMapping ?? context.GetService<IRelationalTypeMappingSource>().FindMapping(operand.Type); return new AtTimeZoneExpression(operand, timeZone, typeof(DateTimeOffset), typeMapping); });
步骤3:原有查询代码可继续使用
修复后,你原来的分组查询代码即可正常运行,不会再出现存储类型为NULL的错误。
内容的提问来源于stack exchange,提问作者Danyil Malko
相关产品推荐
相关产品推荐

