c# - 在 MongoDb 中执行 UpdateAsync
问题描述
我有以下类结构。我试图通过仅传递对象的一部分来调用 UpdateAsync。出于某种原因,它仅在根对象级别 TestObject 类中尊重 BsonIgnoreIfDefault,而不是在 TestProduct 上。
public class TestObject
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
[BsonIgnoreIfDefault]
public string Id { get; set; }
[Required]
public string KoId { get; set; }
[BsonIgnoreIfDefault]
public string Summary { get; set; }
public TestProduct Product { get; set; }
}
public class TestProduct
{
[BsonIgnoreIfDefault]
public string Name { get; set; }
[BsonIgnoreIfDefault]
public List<string> Skus { get; set; }
}
这是我的集成测试的片段:
public async Task EndToEndHappyPath()
{
const string summary = "This is a summary";
var obj = new TestObject
{
Summary = summary,
KoaId = "1234",
Product = new TestProduct
{
Name = "laptop",
Skus = new List<string>
{
"Memory"
}
}
};
// CREATE
await _mongoAsyncRepository.CreateAsync(obj);
obj = new TestObject
{
KoaId = koaId,
Description = description,
Product = new TestProduct
{
Skus = new List<string>
{
"RAM"
}
}
};
// UPDATE
var response = await _mongoAsyncRepository.UpdateAsync(koaId, obj);
response.ShouldBeTrue();
// RETRIEVE
result = await _mongoAsyncRepository.RetrieveOneAsync(koaId);
testObject = (result as TestObject);
testObject.Product.ShouldNotBeNull();
// this is failing; Name value is null in MongoDb
testObject.Product.Name.ShouldBe("laptop");
testObject.Product.Skus.ShouldNotBeNull();
testObject.Product.Skus.Count.ShouldBe(1);
testObject.Product.Skus[0].ShouldBe("RAM");
}
public async Task<bool> UpdateAsync(string id, T obj)
{
try
{
_logger.Log(new KoaLogEntry(KoaLogLevel.Debug, $"Attempting to update a {typeof(T)} {id} document."));
//var actionResult = await GetMongoCollection()?.ReplaceOneAsync(new BsonDocument("KoaId", id), obj);
var updated = new BsonDocument
{
{
"$set", bsonDoc
}
};
UpdateDefinition<BsonDocument> updatedObj = UpdateBuilder.DefinitionFor(updated);
var actionResult = await GetMongoCollection()?.UpdateOneAsync(new BsonDocument("KoaId", id), updated);
_logger.Log(new KoaLogEntry(KoaLogLevel.Debug, $"Updated a {typeof(T)} {id} document. IsAcknowledged = {actionResult.IsAcknowledged}; ModifiedCount = {actionResult.ModifiedCount}"));
return actionResult.IsAcknowledged
&& actionResult.ModifiedCount > 0;
}
catch (Exception exc)
{
_logger.Log(new KoaLogEntry(KoaLogLevel.Error, exc.Message, exc));
throw;
}
}
private readonly IMongoClient _client;
protected IMongoCollection<T> GetMongoCollection()
{
var database = _client.GetDatabase(this.DatabaseName);
return database.GetCollection<T>(typeof(T).Name);
}
出于某种原因,尽管我在其上放置了 BsonIgnoreIfDefault 属性,但 Name 被覆盖为 null。
请让我知道我错过了什么。谢谢阿伦
解决方案
我做了一些研究,似乎不支持开箱即用。
BsonIgnoreIfDefault 的意思是“如果默认,则不包含在数据库中的文档中”,并不意味着“忽略更新”。
您的更新命令
var actionResult = await GetMongoCollection()?.UpdateOneAsync(new BsonDocument("KoaId", id), updated);
应该具有与此相同的行为:
await GetMongoCollection().ReplaceOneAsync(_ => _.KoaId == id, obj);
它将替换现有文档。
文档说(我假设 c# 驱动程序没有魔法):
如果文档仅包含 field:value 表达式,则: update() 方法将匹配的文档替换为文档。update() 方法不会替换 _id 值。有关示例,请参阅替换所有字段。
https://docs.mongodb.com/manual/reference/method/db.collection.update/
因此,您正在进行替换,并且所有具有默认值的属性都不会写入新的新文档:
// document after replace without annotations (pseudocode, fragment only)
{
KoaId: "abc",
Summary: null
}
// with Summary annotated with BsonIgnoreIfDefault
{
KoaId: "abc"
}
我找到的唯一解决方案是编写一个从对象创建 UpdateDefinitions 并添加自定义属性的构建器。这是我的第一个版本,可能会有所帮助:
/// <summary>
/// Ignore property in updates build with UpdateBuilder.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class BsonUpdateIgnoreAttribute : Attribute
{
}
/// <summary>
/// Ignore this property in UpdateBuild if it's value is null
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class BsonUpdateIgnoreIfNullAttribute : Attribute
{
}
public static class UpdateBuilder
{
public static UpdateDefinition<TDocument> DefinitionFor<TDocument>(TDocument document)
{
if (document == null) throw new ArgumentNullException(nameof(document));
var updates = _getUpdateDefinitions<TDocument>("", document);
return Builders<TDocument>.Update.Combine(updates);
}
private static IList<UpdateDefinition<TDocument>> _getUpdateDefinitions<TDocument>(string prefix, object root)
{
var properties = root.GetType().GetProperties();
return properties
.Where(p => p.GetCustomAttribute<BsonUpdateIgnoreAttribute>() == null)
.Where(p => p.GetCustomAttribute<BsonUpdateIgnoreIfNullAttribute>() == null || p.GetValue(root) != null)
.Select(p => _getUpdateDefinition<TDocument>(p, prefix, root)).ToList();
}
private static UpdateDefinition<TDocument> _getUpdateDefinition<TDocument>(PropertyInfo propertyInfo,
string prefix,
object obj)
{
if (propertyInfo.PropertyType.IsClass &&
!propertyInfo.PropertyType.Namespace.AsSpan().StartsWith("System") &&
propertyInfo.GetValue(obj) != null)
{
prefix = prefix + propertyInfo.Name + ".";
return Builders<TDocument>.Update.Combine(
_getUpdateDefinitions<TDocument>(prefix, propertyInfo.GetValue(obj)));
}
return Builders<TDocument>.Update.Set(prefix + propertyInfo.Name, propertyInfo.GetValue(obj));
}
}
请注意,这没有针对性能进行优化。
你可以像这样使用它:
var updateDef = UpdateBuilder.DefinitionFor(updatedDocument);
await Collection.UpdateOneAsync(_ => _.Id == id, updateDef);