C#使用Mongo Driver设置Personio缓存lastModified字段异常问题
问题分析与修复
核心问题
管道顺序与参数传递错误
先用$replaceWith会直接覆盖旧文档,后续$function中的$ROOT已经是新文档,根本无法获取旧数据;同时$function的第二个参数传入的自定义对象并非实际新文档,逻辑完全错误。DateTime默认值问题
C#中DateTime类型的默认值是0001-01-01,当MongoDB映射时如果没有赋值,就会存入这个默认的ISODate。
修复方案
方案1:调整管道顺序,先计算再替换
先基于旧文档计算_lastModified,再合并新数据:
public Task<UpdateResult[]> Save(List<StoredDynamicRecord> personioRecords, CancellationToken cancellationToken) { IMongoCollection<StoredDynamicRecord> collection = GetCollection<StoredDynamicRecord>(); IEnumerable<Task<UpdateResult>> updateResultTasks = personioRecords.Select(personioRecord => { string id = (string)personioRecord.AttributesByName["id"].Value; personioRecord.CacheId = string.IsNullOrEmpty(id) ? ObjectId.GenerateNewId().ToString() : id; var syncTimestamp = DateTime.UtcNow; personioRecord.SyncTimestamp = syncTimestamp; var newDocBson = personioRecord.ToBsonDocument(); return collection.UpdateOneAsync( filter: Builders<StoredDynamicRecord>.Filter.Eq(x => x.CacheId, personioRecord.CacheId), update: new EmptyPipelineDefinition<StoredDynamicRecord>() // 先计算_lastModified:新增用syncTimestamp,更新且数据变化也用syncTimestamp,否则保留旧值 .AppendStage(@"{ $addFields: { _lastModified: { $function: { lang: ""js"", args: [""$$ROOT"", " + BsonValue.Create(syncTimestamp).ToJson() + @"], body: ""function(oldDoc, newSyncTime) { if (!oldDoc) return newSyncTime; const oldAttrs = oldDoc.AttributesByName; const newAttrs = " + newDocBson["AttributesByName"].ToJson() + @"; return JSON.stringify(oldAttrs) !== JSON.stringify(newAttrs) ? newSyncTime : oldDoc._lastModified; }"" } } } }") // 合并新文档与计算好的_lastModified .AppendStage(@"{ $replaceWith: { $mergeObjects: [ " + newDocBson.ToJson() + @", { _lastModified: ""$_lastModified"" } ] } }"), options: new UpdateOptions { IsUpsert = true }, cancellationToken ); }); return Task.WhenAll(updateResultTasks); }
方案2:用MongoDB原生操作符替代JS函数(性能更优)
避免使用JavaScript函数,改用原生聚合操作符实现逻辑:
public Task<UpdateResult[]> Save(List<StoredDynamicRecord> personioRecords, CancellationToken cancellationToken) { IMongoCollection<StoredDynamicRecord> collection = GetCollection<StoredDynamicRecord>(); IEnumerable<Task<UpdateResult>> updateResultTasks = personioRecords.Select(personioRecord => { string id = (string)personioRecord.AttributesByName["id"].Value; personioRecord.CacheId = string.IsNullOrEmpty(id) ? ObjectId.GenerateNewId().ToString() : id; var syncTimestamp = DateTime.UtcNow; personioRecord.SyncTimestamp = syncTimestamp; var newAttrs = personioRecord.AttributesByName; var updateDefinition = Builders<StoredDynamicRecord>.Update .Set(x => x.SyncTimestamp, syncTimestamp) .Set(x => x.AttributesByName, newAttrs) // 用$cond判断:新增或属性变化时设置为syncTimestamp,否则保留原值 .Set(x => x.LastModified, new BsonDocument("$cond", new BsonArray { new BsonDocument("$or", new BsonArray { new BsonDocument("$eq", new BsonArray { "$$ROOT", BsonNull.Value }), // 新增文档 new BsonDocument("$ne", new BsonArray { "$$ROOT.AttributesByName", BsonValue.Create(newAttrs) }) // 属性变化 }), syncTimestamp, "$$ROOT._lastModified" })); return collection.UpdateOneAsync( filter: Builders<StoredDynamicRecord>.Filter.Eq(x => x.CacheId, personioRecord.CacheId), update: updateDefinition, options: new UpdateOptions { IsUpsert = true }, cancellationToken ); }); return Task.WhenAll(updateResultTasks); }
额外注意事项
- 确保
AttributesByName能被MongoDB正确序列化/反序列化,否则对比会失效; - 大型文档推荐用方案2,原生操作符性能远优于JS函数;
- 可以给
LastModified设置默认值:public DateTime LastModified { get; set; } = DateTime.UtcNow;,避免出现默认的0001-01-01值。
内容的提问来源于stack exchange,提问作者Brian Kessler
相关产品推荐
相关产品推荐

