You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 15:47:16