Doing an UpdateAsync in MongoDb












0















I have the below class structure. I'm trying to call UpdateAsync by passing only a part of the object. For some reason it is respecting the BsonIgnoreIfDefault only at the root object level TestObject class, but not on 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; }
}


Here's a snippet of my integration test:



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


For some reason Name is getting overwritten to null though I have put the BsonIgnoreIfDefault attribute on it.



Please let me know what I'm missing.
Thanks
Arun










share|improve this question

























  • It looks like you're using a repository lib? Which lib and what's the type of _mongoAsyncRepository?

    – Christoph Lütjen
    Nov 13 '18 at 14:23











  • Good point @ChristophLütjen. Internally it calls the MongoDB.Driver.MongoClient library.

    – Arun
    Nov 13 '18 at 14:27











  • I find it strange that no one has encountered this problem. Please assist.

    – Arun
    Nov 17 '18 at 9:23











  • it's the code in your repository that's not working as you expect it to work. Could you please provide this code?

    – Christoph Lütjen
    Nov 18 '18 at 14:28











  • @ChristophLütjen.. updated the main question with the method details.

    – Arun
    Nov 19 '18 at 6:57
















0















I have the below class structure. I'm trying to call UpdateAsync by passing only a part of the object. For some reason it is respecting the BsonIgnoreIfDefault only at the root object level TestObject class, but not on 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; }
}


Here's a snippet of my integration test:



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


For some reason Name is getting overwritten to null though I have put the BsonIgnoreIfDefault attribute on it.



Please let me know what I'm missing.
Thanks
Arun










share|improve this question

























  • It looks like you're using a repository lib? Which lib and what's the type of _mongoAsyncRepository?

    – Christoph Lütjen
    Nov 13 '18 at 14:23











  • Good point @ChristophLütjen. Internally it calls the MongoDB.Driver.MongoClient library.

    – Arun
    Nov 13 '18 at 14:27











  • I find it strange that no one has encountered this problem. Please assist.

    – Arun
    Nov 17 '18 at 9:23











  • it's the code in your repository that's not working as you expect it to work. Could you please provide this code?

    – Christoph Lütjen
    Nov 18 '18 at 14:28











  • @ChristophLütjen.. updated the main question with the method details.

    – Arun
    Nov 19 '18 at 6:57














0












0








0








I have the below class structure. I'm trying to call UpdateAsync by passing only a part of the object. For some reason it is respecting the BsonIgnoreIfDefault only at the root object level TestObject class, but not on 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; }
}


Here's a snippet of my integration test:



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


For some reason Name is getting overwritten to null though I have put the BsonIgnoreIfDefault attribute on it.



Please let me know what I'm missing.
Thanks
Arun










share|improve this question
















I have the below class structure. I'm trying to call UpdateAsync by passing only a part of the object. For some reason it is respecting the BsonIgnoreIfDefault only at the root object level TestObject class, but not on 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; }
}


Here's a snippet of my integration test:



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


For some reason Name is getting overwritten to null though I have put the BsonIgnoreIfDefault attribute on it.



Please let me know what I'm missing.
Thanks
Arun







c# mongodb






share|improve this question















share|improve this question













share|improve this question




share|improve this question








edited Dec 8 '18 at 8:04







Arun

















asked Nov 13 '18 at 13:44









ArunArun

650718




650718













  • It looks like you're using a repository lib? Which lib and what's the type of _mongoAsyncRepository?

    – Christoph Lütjen
    Nov 13 '18 at 14:23











  • Good point @ChristophLütjen. Internally it calls the MongoDB.Driver.MongoClient library.

    – Arun
    Nov 13 '18 at 14:27











  • I find it strange that no one has encountered this problem. Please assist.

    – Arun
    Nov 17 '18 at 9:23











  • it's the code in your repository that's not working as you expect it to work. Could you please provide this code?

    – Christoph Lütjen
    Nov 18 '18 at 14:28











  • @ChristophLütjen.. updated the main question with the method details.

    – Arun
    Nov 19 '18 at 6:57



















  • It looks like you're using a repository lib? Which lib and what's the type of _mongoAsyncRepository?

    – Christoph Lütjen
    Nov 13 '18 at 14:23











  • Good point @ChristophLütjen. Internally it calls the MongoDB.Driver.MongoClient library.

    – Arun
    Nov 13 '18 at 14:27











  • I find it strange that no one has encountered this problem. Please assist.

    – Arun
    Nov 17 '18 at 9:23











  • it's the code in your repository that's not working as you expect it to work. Could you please provide this code?

    – Christoph Lütjen
    Nov 18 '18 at 14:28











  • @ChristophLütjen.. updated the main question with the method details.

    – Arun
    Nov 19 '18 at 6:57

















It looks like you're using a repository lib? Which lib and what's the type of _mongoAsyncRepository?

– Christoph Lütjen
Nov 13 '18 at 14:23





It looks like you're using a repository lib? Which lib and what's the type of _mongoAsyncRepository?

– Christoph Lütjen
Nov 13 '18 at 14:23













Good point @ChristophLütjen. Internally it calls the MongoDB.Driver.MongoClient library.

– Arun
Nov 13 '18 at 14:27





Good point @ChristophLütjen. Internally it calls the MongoDB.Driver.MongoClient library.

– Arun
Nov 13 '18 at 14:27













I find it strange that no one has encountered this problem. Please assist.

– Arun
Nov 17 '18 at 9:23





I find it strange that no one has encountered this problem. Please assist.

– Arun
Nov 17 '18 at 9:23













it's the code in your repository that's not working as you expect it to work. Could you please provide this code?

– Christoph Lütjen
Nov 18 '18 at 14:28





it's the code in your repository that's not working as you expect it to work. Could you please provide this code?

– Christoph Lütjen
Nov 18 '18 at 14:28













@ChristophLütjen.. updated the main question with the method details.

– Arun
Nov 19 '18 at 6:57





@ChristophLütjen.. updated the main question with the method details.

– Arun
Nov 19 '18 at 6:57












1 Answer
1






active

oldest

votes


















0














I did some research and it seems that this is not supported out of the box.



BsonIgnoreIfDefault means "do not include in document in db if default" it does NOT mean "ignore in updates".



Your update command



var actionResult = await GetMongoCollection()?.UpdateOneAsync(new BsonDocument("KoaId", id), updated);


should have the same behavior as this:



await GetMongoCollection().ReplaceOneAsync(_ => _.KoaId == id, obj);


It will replace the the existing document.



The docs say (and I assume, that the c# driver does no magic):




If the document contains only field:value expressions, then:
The update() method replaces the matching document with the document. The update() method does not replace the _id value. For an example, see Replace All Fields.




https://docs.mongodb.com/manual/reference/method/db.collection.update/



So you're doing a replace and all properties having default values will not be written to new new document:



// document after replace without annotations (pseudocode, fragment only)
{
KoaId: "abc",
Summary: null
}

// with Summary annotated with BsonIgnoreIfDefault
{
KoaId: "abc"
}


The only solution I found, is to write a builder that creates UpdateDefinitions from an object and add custom attributes. This is my first version that may help as a start:



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


Please not that this is not optimized for performance.



You can use it like so:



        var updateDef = UpdateBuilder.DefinitionFor(updatedDocument);
await Collection.UpdateOneAsync(_ => _.Id == id, updateDef);





share|improve this answer
























  • Getting serialization issue: Parameter count mismatch. I've updated my code in the original post to reflect your suggestion as well. On second thoughts, if this is this complex, isn't it easier to just read the entire document from the repo, update parts of it and save it again? Obviously not efficient, but would work right? Something for MongoDB team to work on I guess!

    – Arun
    Dec 8 '18 at 8:06








  • 1





    Reading the record first is a common option. (while you still have the work to compare old and new document). Another option is to think in "commands" that are made for a special partial update. E.g. we have a "user" document and an update method "UpdatePassword" that accepts the new password and will do exactly this (using a single set operation). If you can split you use cases I'd recommend to go that route.

    – Christoph Lütjen
    Dec 8 '18 at 15:18











  • Sir, your suggestion of using specific commands is in the works for version 2.. where we've split the document into smaller chunks. Due to lack of time, we'll read the document, update parts of it and save it. I want to know what to do with ticket. I don't want to mark it as answer since it didn't work for me, but I'd like to give you credit for your time and assistance. Please advise.

    – Arun
    Dec 9 '18 at 4:20











  • Your question was "why is BsonIgnoreIfDefault attribute not working". My answer is "because you misunderstood the meaning of this attribute. So I'd say it's the correct answer. But if you don't want to accept it, that's totally ok for me.

    – Christoph Lütjen
    Dec 9 '18 at 11:16











  • Sir, makes sense. Marked your post as the answer.

    – Arun
    Dec 11 '18 at 3:28











Your Answer






StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");

StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);

StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});

function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});


}
});














draft saved

draft discarded


















StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53282401%2fdoing-an-updateasync-in-mongodb%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown

























1 Answer
1






active

oldest

votes








1 Answer
1






active

oldest

votes









active

oldest

votes






active

oldest

votes









0














I did some research and it seems that this is not supported out of the box.



BsonIgnoreIfDefault means "do not include in document in db if default" it does NOT mean "ignore in updates".



Your update command



var actionResult = await GetMongoCollection()?.UpdateOneAsync(new BsonDocument("KoaId", id), updated);


should have the same behavior as this:



await GetMongoCollection().ReplaceOneAsync(_ => _.KoaId == id, obj);


It will replace the the existing document.



The docs say (and I assume, that the c# driver does no magic):




If the document contains only field:value expressions, then:
The update() method replaces the matching document with the document. The update() method does not replace the _id value. For an example, see Replace All Fields.




https://docs.mongodb.com/manual/reference/method/db.collection.update/



So you're doing a replace and all properties having default values will not be written to new new document:



// document after replace without annotations (pseudocode, fragment only)
{
KoaId: "abc",
Summary: null
}

// with Summary annotated with BsonIgnoreIfDefault
{
KoaId: "abc"
}


The only solution I found, is to write a builder that creates UpdateDefinitions from an object and add custom attributes. This is my first version that may help as a start:



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


Please not that this is not optimized for performance.



You can use it like so:



        var updateDef = UpdateBuilder.DefinitionFor(updatedDocument);
await Collection.UpdateOneAsync(_ => _.Id == id, updateDef);





share|improve this answer
























  • Getting serialization issue: Parameter count mismatch. I've updated my code in the original post to reflect your suggestion as well. On second thoughts, if this is this complex, isn't it easier to just read the entire document from the repo, update parts of it and save it again? Obviously not efficient, but would work right? Something for MongoDB team to work on I guess!

    – Arun
    Dec 8 '18 at 8:06








  • 1





    Reading the record first is a common option. (while you still have the work to compare old and new document). Another option is to think in "commands" that are made for a special partial update. E.g. we have a "user" document and an update method "UpdatePassword" that accepts the new password and will do exactly this (using a single set operation). If you can split you use cases I'd recommend to go that route.

    – Christoph Lütjen
    Dec 8 '18 at 15:18











  • Sir, your suggestion of using specific commands is in the works for version 2.. where we've split the document into smaller chunks. Due to lack of time, we'll read the document, update parts of it and save it. I want to know what to do with ticket. I don't want to mark it as answer since it didn't work for me, but I'd like to give you credit for your time and assistance. Please advise.

    – Arun
    Dec 9 '18 at 4:20











  • Your question was "why is BsonIgnoreIfDefault attribute not working". My answer is "because you misunderstood the meaning of this attribute. So I'd say it's the correct answer. But if you don't want to accept it, that's totally ok for me.

    – Christoph Lütjen
    Dec 9 '18 at 11:16











  • Sir, makes sense. Marked your post as the answer.

    – Arun
    Dec 11 '18 at 3:28
















0














I did some research and it seems that this is not supported out of the box.



BsonIgnoreIfDefault means "do not include in document in db if default" it does NOT mean "ignore in updates".



Your update command



var actionResult = await GetMongoCollection()?.UpdateOneAsync(new BsonDocument("KoaId", id), updated);


should have the same behavior as this:



await GetMongoCollection().ReplaceOneAsync(_ => _.KoaId == id, obj);


It will replace the the existing document.



The docs say (and I assume, that the c# driver does no magic):




If the document contains only field:value expressions, then:
The update() method replaces the matching document with the document. The update() method does not replace the _id value. For an example, see Replace All Fields.




https://docs.mongodb.com/manual/reference/method/db.collection.update/



So you're doing a replace and all properties having default values will not be written to new new document:



// document after replace without annotations (pseudocode, fragment only)
{
KoaId: "abc",
Summary: null
}

// with Summary annotated with BsonIgnoreIfDefault
{
KoaId: "abc"
}


The only solution I found, is to write a builder that creates UpdateDefinitions from an object and add custom attributes. This is my first version that may help as a start:



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


Please not that this is not optimized for performance.



You can use it like so:



        var updateDef = UpdateBuilder.DefinitionFor(updatedDocument);
await Collection.UpdateOneAsync(_ => _.Id == id, updateDef);





share|improve this answer
























  • Getting serialization issue: Parameter count mismatch. I've updated my code in the original post to reflect your suggestion as well. On second thoughts, if this is this complex, isn't it easier to just read the entire document from the repo, update parts of it and save it again? Obviously not efficient, but would work right? Something for MongoDB team to work on I guess!

    – Arun
    Dec 8 '18 at 8:06








  • 1





    Reading the record first is a common option. (while you still have the work to compare old and new document). Another option is to think in "commands" that are made for a special partial update. E.g. we have a "user" document and an update method "UpdatePassword" that accepts the new password and will do exactly this (using a single set operation). If you can split you use cases I'd recommend to go that route.

    – Christoph Lütjen
    Dec 8 '18 at 15:18











  • Sir, your suggestion of using specific commands is in the works for version 2.. where we've split the document into smaller chunks. Due to lack of time, we'll read the document, update parts of it and save it. I want to know what to do with ticket. I don't want to mark it as answer since it didn't work for me, but I'd like to give you credit for your time and assistance. Please advise.

    – Arun
    Dec 9 '18 at 4:20











  • Your question was "why is BsonIgnoreIfDefault attribute not working". My answer is "because you misunderstood the meaning of this attribute. So I'd say it's the correct answer. But if you don't want to accept it, that's totally ok for me.

    – Christoph Lütjen
    Dec 9 '18 at 11:16











  • Sir, makes sense. Marked your post as the answer.

    – Arun
    Dec 11 '18 at 3:28














0












0








0







I did some research and it seems that this is not supported out of the box.



BsonIgnoreIfDefault means "do not include in document in db if default" it does NOT mean "ignore in updates".



Your update command



var actionResult = await GetMongoCollection()?.UpdateOneAsync(new BsonDocument("KoaId", id), updated);


should have the same behavior as this:



await GetMongoCollection().ReplaceOneAsync(_ => _.KoaId == id, obj);


It will replace the the existing document.



The docs say (and I assume, that the c# driver does no magic):




If the document contains only field:value expressions, then:
The update() method replaces the matching document with the document. The update() method does not replace the _id value. For an example, see Replace All Fields.




https://docs.mongodb.com/manual/reference/method/db.collection.update/



So you're doing a replace and all properties having default values will not be written to new new document:



// document after replace without annotations (pseudocode, fragment only)
{
KoaId: "abc",
Summary: null
}

// with Summary annotated with BsonIgnoreIfDefault
{
KoaId: "abc"
}


The only solution I found, is to write a builder that creates UpdateDefinitions from an object and add custom attributes. This is my first version that may help as a start:



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


Please not that this is not optimized for performance.



You can use it like so:



        var updateDef = UpdateBuilder.DefinitionFor(updatedDocument);
await Collection.UpdateOneAsync(_ => _.Id == id, updateDef);





share|improve this answer













I did some research and it seems that this is not supported out of the box.



BsonIgnoreIfDefault means "do not include in document in db if default" it does NOT mean "ignore in updates".



Your update command



var actionResult = await GetMongoCollection()?.UpdateOneAsync(new BsonDocument("KoaId", id), updated);


should have the same behavior as this:



await GetMongoCollection().ReplaceOneAsync(_ => _.KoaId == id, obj);


It will replace the the existing document.



The docs say (and I assume, that the c# driver does no magic):




If the document contains only field:value expressions, then:
The update() method replaces the matching document with the document. The update() method does not replace the _id value. For an example, see Replace All Fields.




https://docs.mongodb.com/manual/reference/method/db.collection.update/



So you're doing a replace and all properties having default values will not be written to new new document:



// document after replace without annotations (pseudocode, fragment only)
{
KoaId: "abc",
Summary: null
}

// with Summary annotated with BsonIgnoreIfDefault
{
KoaId: "abc"
}


The only solution I found, is to write a builder that creates UpdateDefinitions from an object and add custom attributes. This is my first version that may help as a start:



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


Please not that this is not optimized for performance.



You can use it like so:



        var updateDef = UpdateBuilder.DefinitionFor(updatedDocument);
await Collection.UpdateOneAsync(_ => _.Id == id, updateDef);






share|improve this answer












share|improve this answer



share|improve this answer










answered Dec 6 '18 at 14:58









Christoph LütjenChristoph Lütjen

1,237811




1,237811













  • Getting serialization issue: Parameter count mismatch. I've updated my code in the original post to reflect your suggestion as well. On second thoughts, if this is this complex, isn't it easier to just read the entire document from the repo, update parts of it and save it again? Obviously not efficient, but would work right? Something for MongoDB team to work on I guess!

    – Arun
    Dec 8 '18 at 8:06








  • 1





    Reading the record first is a common option. (while you still have the work to compare old and new document). Another option is to think in "commands" that are made for a special partial update. E.g. we have a "user" document and an update method "UpdatePassword" that accepts the new password and will do exactly this (using a single set operation). If you can split you use cases I'd recommend to go that route.

    – Christoph Lütjen
    Dec 8 '18 at 15:18











  • Sir, your suggestion of using specific commands is in the works for version 2.. where we've split the document into smaller chunks. Due to lack of time, we'll read the document, update parts of it and save it. I want to know what to do with ticket. I don't want to mark it as answer since it didn't work for me, but I'd like to give you credit for your time and assistance. Please advise.

    – Arun
    Dec 9 '18 at 4:20











  • Your question was "why is BsonIgnoreIfDefault attribute not working". My answer is "because you misunderstood the meaning of this attribute. So I'd say it's the correct answer. But if you don't want to accept it, that's totally ok for me.

    – Christoph Lütjen
    Dec 9 '18 at 11:16











  • Sir, makes sense. Marked your post as the answer.

    – Arun
    Dec 11 '18 at 3:28



















  • Getting serialization issue: Parameter count mismatch. I've updated my code in the original post to reflect your suggestion as well. On second thoughts, if this is this complex, isn't it easier to just read the entire document from the repo, update parts of it and save it again? Obviously not efficient, but would work right? Something for MongoDB team to work on I guess!

    – Arun
    Dec 8 '18 at 8:06








  • 1





    Reading the record first is a common option. (while you still have the work to compare old and new document). Another option is to think in "commands" that are made for a special partial update. E.g. we have a "user" document and an update method "UpdatePassword" that accepts the new password and will do exactly this (using a single set operation). If you can split you use cases I'd recommend to go that route.

    – Christoph Lütjen
    Dec 8 '18 at 15:18











  • Sir, your suggestion of using specific commands is in the works for version 2.. where we've split the document into smaller chunks. Due to lack of time, we'll read the document, update parts of it and save it. I want to know what to do with ticket. I don't want to mark it as answer since it didn't work for me, but I'd like to give you credit for your time and assistance. Please advise.

    – Arun
    Dec 9 '18 at 4:20











  • Your question was "why is BsonIgnoreIfDefault attribute not working". My answer is "because you misunderstood the meaning of this attribute. So I'd say it's the correct answer. But if you don't want to accept it, that's totally ok for me.

    – Christoph Lütjen
    Dec 9 '18 at 11:16











  • Sir, makes sense. Marked your post as the answer.

    – Arun
    Dec 11 '18 at 3:28

















Getting serialization issue: Parameter count mismatch. I've updated my code in the original post to reflect your suggestion as well. On second thoughts, if this is this complex, isn't it easier to just read the entire document from the repo, update parts of it and save it again? Obviously not efficient, but would work right? Something for MongoDB team to work on I guess!

– Arun
Dec 8 '18 at 8:06







Getting serialization issue: Parameter count mismatch. I've updated my code in the original post to reflect your suggestion as well. On second thoughts, if this is this complex, isn't it easier to just read the entire document from the repo, update parts of it and save it again? Obviously not efficient, but would work right? Something for MongoDB team to work on I guess!

– Arun
Dec 8 '18 at 8:06






1




1





Reading the record first is a common option. (while you still have the work to compare old and new document). Another option is to think in "commands" that are made for a special partial update. E.g. we have a "user" document and an update method "UpdatePassword" that accepts the new password and will do exactly this (using a single set operation). If you can split you use cases I'd recommend to go that route.

– Christoph Lütjen
Dec 8 '18 at 15:18





Reading the record first is a common option. (while you still have the work to compare old and new document). Another option is to think in "commands" that are made for a special partial update. E.g. we have a "user" document and an update method "UpdatePassword" that accepts the new password and will do exactly this (using a single set operation). If you can split you use cases I'd recommend to go that route.

– Christoph Lütjen
Dec 8 '18 at 15:18













Sir, your suggestion of using specific commands is in the works for version 2.. where we've split the document into smaller chunks. Due to lack of time, we'll read the document, update parts of it and save it. I want to know what to do with ticket. I don't want to mark it as answer since it didn't work for me, but I'd like to give you credit for your time and assistance. Please advise.

– Arun
Dec 9 '18 at 4:20





Sir, your suggestion of using specific commands is in the works for version 2.. where we've split the document into smaller chunks. Due to lack of time, we'll read the document, update parts of it and save it. I want to know what to do with ticket. I don't want to mark it as answer since it didn't work for me, but I'd like to give you credit for your time and assistance. Please advise.

– Arun
Dec 9 '18 at 4:20













Your question was "why is BsonIgnoreIfDefault attribute not working". My answer is "because you misunderstood the meaning of this attribute. So I'd say it's the correct answer. But if you don't want to accept it, that's totally ok for me.

– Christoph Lütjen
Dec 9 '18 at 11:16





Your question was "why is BsonIgnoreIfDefault attribute not working". My answer is "because you misunderstood the meaning of this attribute. So I'd say it's the correct answer. But if you don't want to accept it, that's totally ok for me.

– Christoph Lütjen
Dec 9 '18 at 11:16













Sir, makes sense. Marked your post as the answer.

– Arun
Dec 11 '18 at 3:28





Sir, makes sense. Marked your post as the answer.

– Arun
Dec 11 '18 at 3:28


















draft saved

draft discarded




















































Thanks for contributing an answer to Stack Overflow!


  • Please be sure to answer the question. Provide details and share your research!

But avoid



  • Asking for help, clarification, or responding to other answers.

  • Making statements based on opinion; back them up with references or personal experience.


To learn more, see our tips on writing great answers.




draft saved


draft discarded














StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53282401%2fdoing-an-updateasync-in-mongodb%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown





















































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown

































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown







Popular posts from this blog

Bressuire

Vorschmack

Quarantine