首页 > 解决方案 > 在 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。

请让我知道我错过了什么。谢谢阿伦

标签: c#mongodb

解决方案


我做了一些研究,似乎不支持开箱即用。

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);

推荐阅读