.NET 8中Hangfire.PostgreSql调用GetStateData时反序列化异常排查
核心问题本质
这个异常的根源是Newtonsoft.Json无法反序列化Hangfire.PostgreSql内部的SafeDictionary<TKey,TValue>类型——该类型是PostgreSqlMonitoringApi的私有内部类,没有公开的无参构造函数,而Hangfire的序列化流程在某些场景下错误地尝试将数据库中的状态JSON反序列化为这个内部类型,而非公共的IDictionary<string,string>或Dictionary<string,string>。
具体成因
版本不兼容
Hangfire.Core与Hangfire.PostgreSql的版本未对齐,导致两者在状态数据的序列化/反序列化逻辑上出现分歧:比如新版本的PostgreSql存储库改用SafeDictionary包装状态数据,但旧版本的Core无法识别这个内部类型的构造规则;反之亦然。并发竞态条件
当Hangfire后台进程正在更新任务状态(比如任务刚完成、失败或被重试)时,数据库中的状态JSON可能处于半写入的中间状态——此时你的GetStateData调用刚好读取到这个未完全写入的、带有SafeDictionary类型标记的JSON,触发反序列化失败。这也是异常间歇性出现的核心原因。序列化配置被篡改
如果你的项目自定义了Hangfire使用的Newtonsoft.Json序列化配置(比如修改了ConstructorHandling选项),可能导致序列化器无法识别SafeDictionary的非公开构造函数,进而抛出无默认构造的异常。
解决步骤
对齐版本
检查并升级Hangfire.Core和Hangfire.PostgreSql到最新稳定兼容版本(优先使用NuGet上的匹配版本,比如两者都用1.8.x系列),确保序列化逻辑一致。添加重试机制
针对间歇性的竞态问题,在调用GetStateData时添加重试逻辑:
private StateData GetJobState(string jobId, int retryCount = 3) { var retryAttempts = 0; do { try { using (var connection = JobStorage.Current.GetConnection()) { return connection.GetStateData(jobId); } } catch (JsonSerializationException ex) when (ex.Message.Contains("SafeDictionary")) { retryAttempts++; if (retryAttempts >= retryCount) throw; Thread.Sleep(100); } } while (retryAttempts < retryCount); return null; }
- 改用公共API获取状态
避免直接调用底层的GetStateData,改用Hangfire的公共IMonitoringApi来获取任务状态,它的实现更稳定,不会依赖内部类型:
private string GetJobCurrentState(string jobId) { var monitoringApi = JobStorage.Current.GetMonitoringApi(); var jobDetails = monitoringApi.JobDetails(jobId); return jobDetails.History.FirstOrDefault()?.State; }
- 修复序列化配置
如果自定义了Json序列化器,确保添加允许非公开构造函数的配置:
GlobalConfiguration.Configuration.UseSerializerSettings(new JsonSerializerSettings { ConstructorHandling = ConstructorHandling.AllowNonPublicConstructors });
内容的提问来源于stack exchange,提问作者ctor

