DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

MongoDB C# 更新报 immutable field _id:查询投影不会禁止写入 ID

C# 查询排除 _id 后仍可能通过反射 Set 写入映射属性。核对最终更新定义,用允许更新的字段构造请求,并保留目标 ID。

MongoDB C# 应用查询时已经用 Projection 排除了 _id,更新记录仍报 Performing an update on the path '_id' would modify the immutable field '_id',错误码可能是 66。先检查更新定义中是否仍包含映射到 _id 的属性,尤其是遍历实体所有属性并逐个 Set 的通用仓储代码。

查询投影只控制这次读取返回什么,不会删除 C# 类里的属性,也不会禁止后续更新写入它。默认的 Id、id 或 _id 属性可以映射到根文档的 BSON _id;还可能有 BsonId 属性或自定义类映射。因此只过滤字符串“_id”,未必能排除真正的身份属性。

缺少 ID 的实体仍然可能被送去更新

查询不返回 _id 后,反序列化对象里的相关属性可能保留默认值。如果更新循环重新取得该属性并加入 Set,服务器会收到一次修改身份字段的请求。默认值、另一个 ID 或错误映射都可能触发拒绝,不能只检查 JSON 页面里是否出现“_id”。

在受控环境核对实体映射和最终更新定义:哪些 CLR 属性映射到哪些 BSON 字段,$set 是否包含 _id 或它的子路径,目标文档的现有身份是什么。诊断时避免输出整份含业务数据或凭据的对象。

模型和更新定义尚未核实时,不要断言一定是某个默认值导致;先找出实际发送的字段。若错误来自 replacement、聚合更新管道或 upsert,应检查对应写法,不能把所有路径都当成同一个反射循环。

用允许更新的字段构造更新定义

业务更新通常只需要修改明确的字段。使用白名单比遍历所有属性后再排除少数特殊名称更容易审查,也不会把新增的身份、审计或内部字段自动变成可写字段。

以下为局部 C# 示例。collection 是已建立连接的 IMongoCollection<UserDocument>,documentId 是已经确认类型和目标的 ObjectId,newName 已完成业务校验;Name、UpdatedOn 是模型中允许更新的属性:

var filter = Builders<UserDocument>.Filter.Eq(
    x => x.Id, documentId);

var update = Builders<UserDocument>.Update
    .Set(x => x.Name, newName)
    .Set(x => x.UpdatedOn, DateTime.UtcNow);

var result = await collection.UpdateOneAsync(filter, update);

过滤条件可以使用 ID 定位文档,更新定义里没有给 ID 重新赋值。实体采用字符串、UUID 或自定义身份字段时,应按实际类型和类映射调整,不能为了绕过报错临时换掉文档的身份类型。

这个例子假定只改两个字段;真实接口要明确哪些字段可以省略、哪些可以设置为 null,不能把未提供的输入默认值无条件覆盖进数据库。不要将整份查询实体或外部 DTO 自动展开成 $set。

若继续维护通用更新器,应通过项目实际的序列化映射识别根文档身份成员,并使用明确的可写字段规则。仅凭 CLR 属性名称与“_id”不相等判断可写,会漏掉 Id 或自定义映射;任意给模型加忽略属性,也可能同时破坏读取和插入行为。

不需要为此删除再插入文档

普通更新应保留原来的 _id。删除原文档再创建一个新文档会影响引用、唯一性、并发读写和审计,不能作为排除更新字段的快捷办法。启用 upsert 也不会让已有文档的 _id 变成可修改字段。

确实需要迁移到另一个身份时,应设计独立的数据迁移流程,先处理关联记录、唯一键、失败恢复与并发条件。这与普通业务字段更新是不同任务,不应塞进每次保存实体的代码。

同时检查匹配数和数据内容

在驱动确认写入结果的前提下,查看 MatchedCount 是否符合预期。ModifiedCount 为零可能表示新值与原值相同,不能单凭它认为保存失败;MatchedCount 为零则应核对过滤条件、身份类型、数据库与集合,而不是临时开启 upsert 创建另一份记录。

验收时重新读取目标文档,确认原 _id 保持不变、允许更新的字段符合请求、未传入的其他字段得到保留。对于配置了未确认写入的入口,应先明确它的结果保障,不能据未提供的计数声称成功。

改通用更新器前保存代码与相关数据,在隔离副本检查已有记录、未匹配、相同值、投影缺少身份字段及不允许更新的输入。出现回归时恢复原应用版本,并对实际已提交数据核对;代码回退不会自动撤销一次已经成功的字段更新。