Upserting in Mongo DB using official C# driver - c#

In the official documentation of mongodb they mention upserts, so it would be really nice to write an upsert command instead of:
if (_campaignRepo.Exists(camp))
{
_campaignRepo.DeleteByIdAndSystemId(camp);
}
_campaignRepo.Save(camp);
something which would implement that logic on the db level if it is possible. So what is the way to do an upsert if there is one?

Version 2 of the MongoDB C# driver requires setting the IsUpsert flag in the write commands. This example will upsert an entire document.
var newDoc = new BsonDocument { { "_id", 123 }, { "someKey", "someValue" } };
var result = await collection.ReplaceOneAsync(
filter: new BsonDocument("_id", 123),
options: new ReplaceOptions { IsUpsert = true },
replacement: newDoc);
Version 1 of the MongoDB C# driver implements this logic within the Save command.
var newDoc = new BsonDocument { { "_id", 123 }, { "someKey", "someValue" } };
collection.Save(newDoc);
The Save method is a combination of Insert and Update. If the Id member of the document has a value, then it is assumed to be an existing document and Save calls Update on the document (setting the Upsert flag just in case it actually is a new document after all). Otherwise it is assumed to be a new document and Save calls Insert after first assigning a newly generated unique value to the Id member.
Reference: http://mongodb.github.io/mongo-csharp-driver/1.11/driver/#save-tdocument-method
Note: This does require the proper mapping of the Id field however. More info on that here: http://mongodb.github.io/mongo-csharp-driver/1.11/serialization/#identifying-the-id-field-or-property

Starting from v2.0 of the driver there's a new async-only API. The old API should no longer be used as it's a blocking facade over the new API and is deprecated.
The currently recommended way to upsert a document is by calling and awaiting ReplaceOneAsync with the IsUpsert flag turned on and a filter that matches the relevant document:
Hamster hamster = ...
var replaceOneResult = await collection.ReplaceOneAsync(
doc => doc.Id == hamster.Id,
hamster,
new UpdateOptions {IsUpsert = true});
You can check whether the operation was an insert or an update by looking at ReplaceOneResult.MatchedCount:

The following code is from a working app:
weekplanStore.Update(
Query.EQ("weekNumber", week),
Update.Replace(rawWeekPlan),
UpdateFlags.Upsert);
The weekplanStore is my MongoDB collection, and the code will update the document found with the query in the first argument or insert a new one if none is found. The "trick" is to use the UpdateFlags.Upsert modifier.
The rawWeekPlan is the object inserted or updated, and has the following type:
private class RawWeekPlan
{
public ObjectId id;
public int weekNumber;
public WeekPlanEntry[] entries;
}
and turned into bson by the driver automatically.

You can use the regular update command, but just pass it the Upsert update flag
MongoCollection collection = db.GetCollection("matches");
var query = new QueryDocument("recordId", recordId);
var update = Update.Set("FirstName", "John").Set("LastName","Doe");
matchCollection.Update(query, update, UpdateFlags.Upsert, SafeMode.False);
That code is adapted from a working application (shortened for clarity)

Related

Deleting a document from Cosmos DB - entity with the specified id does not exist in the system

I'm experiencing an issue when trying to delete a document from CosmosDb, from my understanding I first have to load the document I want to delete and then pass in the document.SelfLink into the following method:
DeleteDocumentAsync
However, when I try to delete it throws the following error:
entity with the specified id does not exist in the system
I know the document exists as I'm able to see the document within the document variable.
Below is my current code setup:
private static void DeleteDocument()
{
var feedOptions = new FeedOptions
{
MaxItemCount = -1,
EnableCrossPartitionQuery = true
};
var document = _documentClient
.CreateDocumentQuery(UriFactory.CreateDocumentCollectionUri(_databaseName, _collection), feedOptions)
.Where(x => x.Id == "ba00d500-de61-411e-b8ef-f749b23cc326")
.AsEnumerable()
.SingleOrDefault();
if (document == null) return; // Document is not null when I check here
_documentClient.DeleteDocumentAsync(UriFactory.CreateDocumentUri(_databaseName, _collection, document.Id), new RequestOptions { PartitionKey = new Microsoft.Azure.Documents.PartitionKey(document.Id) })
.GetAwaiter()
.GetResult();
}
Here is the document in CosmosDb:
I've also tried referring to the document.SelfLink instead of using the UriFactory.CreateDocumentUrl yet the outcome is still the same.
Update.
Seems passing in Undefined.Value as the PartitionKey seems to work:
_documentClient.DeleteDocumentAsync(document.SelfLink, new RequestOptions { PartitionKey = new Microsoft.Azure.Documents.PartitionKey(Undefined.Value) })
.GetAwaiter()
.GetResult();
Which I find really odd personally.
I don't see anything weird, just a need to know the partition key and specify it when sending a request to server. Partition key is part of document identity just as id-field. Even if the partition key is undefined.
When you queried documents, you explicitly requested a scan through all partitions via EnableCrossPartitionQuery = true which allowed you to omit partition key from query and still get results. You should have passed in PArtition key there too, to save costs, but it works.
For delete, there is no cross-partition deletes and you have to pass in the partition key. Once you did, the document was found, and could be deleted. Quite expected.
In addition, NO you don't have to load a document to delete it, that's jsut extra complexity, cost and roundtripto server. All you need to know is the identity of document to be deleted (database + collection + partition + id). For cosmosDB V2 client, a la this example:
var documentUri = UriFactory.CreateDocumentUri(databaseName, containerName, documentId);
var options = new RequestOptions() { PartitionKey = new PartitionKey(partition) };
await cosmosClient.DeleteDocumentAsync(documentUri, options).ConfigureAwait(false);

How to know if a collection exists Mongodb C# Version 2 or older

I'm trying to update some code to the new version of Mongodb driver for c#, version 2 or newer, but it seems that the CollectionExists() method was deprecated, it used to work in the old version but not anymore. How can I know if a collection exists already with the new driver?
public static IMongoCollection<T> GetCollectionSafe<T>(string collectionName)
{
var db = GetDatabase();
if (!db.CollectionExists(collectionName)) //throws error
{
db.CreateCollection(collectionName);
}
return db.GetCollection<T>(collectionName);
}
GetDatabase() is of type IMongoDatabase . I just want to know if a collection with a certain name exists.
You can check existence of the collection by following code
public async Task<bool> CheckCollection(IMongoDatabase database, string collectionName)
{
var filter = new BsonDocument("name", collectionName);
var collectionCursor = await database.ListCollectionsAsync(new ListCollectionsOptions {Filter = filter});
return await collectionCursor.AnyAsync();
}
P.S. Method GetCollection is safe for using. You don't need to check existence of the collection. It was done by driver itself.
This code fails on my database:
database.ListCollections(new ListCollectionsOptions {
Filter = new BsonDocument { { "name", collectionName } } })
I get the following error message:
The GuidRepresentation for the reader is CSharpLegacy, which requires
the binary sub type to be UuidLegacy, not UuidStandard
The collection in question was created the MongoDb sink for Serilog with GuidRepresentation = CSharpLegacy.
This code works without problems (with mongo driver v. 2.5.0):
database.GetCollection<BsonDocument>(collectionName) != null
In new version of MongoDB.Driver 2.9.3 for checking the collection existence and then create it , you can use this code:
var dbset = typeof(T).Name;
var tables = MongoDb.ListCollectionNames().ToList();
if (!tables.Any(x => x == dbset))
{
MongoDb.CreateCollection(dbset);
}
return MongoDb.GetCollection<T>(dbset);
Have you tried by using ?
db.GetCollection("collectionName").Exists()
How to check if collection exists in MongoDB using C# driver?

is filter is replaceable of query in mongo queries using c#?

I'm new to mongo in c#
I found tow ways to find documents based on search critiria:
the first using filter:
var collection = _database.GetCollection<BsonDocument>("restaurants");
var filter = Builders<BsonDocument>.Filter.Eq("address.zipcode", "10075");
var result = await collection.Find(filter).ToListAsync();
The second using query:
MongoCollection<BsonDocument> books;
var query = Query.EQ("author", "Kurt Vonnegut");
foreach (BsonDocument book in books.Find(query)) {
// do something with book
}
What's the best way to find documents based to what MongoDB recommendation?
As far I know query builders (like your second example using Query.EQ) belong to old versions of C# drivers (1.X) (see Query class). Also I suggest you to see Builder section in this link that confirm query builders is the old way to consult data.
After the release of the 2.0 version of the .NET driver, a lot of changes were made, including the way of consult the data (you can read more about that in this link). If you are using the last C# driver version you should use your first approach.
The first way you mentioned is quite fine. You might want to incorporate using a mongo cursor as well to allow you to iterate through results
var collection = _database.GetCollection<BsonDocument>("restaurants");
var filter = Builders<BsonDocument>.Filter.Eq("address.zipcode", "10075");
using(var _cursor = await collection.Find(filter).ToCursorAsync())
{
while(await _cursor.MoveNextAsync())
{
foreach(var _document in _cursor.Current) //gets current document
{
//you can do things like get the _id of the document
var _id = _document["_id"];
}
}
}

MongoDB C# Upsert with Guid

When attempting to perform an upsert operation in Mongo, I'd like to have it generate a GUID for the ID instead of an Object ID. In this case, I'm checking to make sure an object with specific properties doesn't already exist and actually throwing an exception if the update occurs.
Here's a stub of the class definition:
public class Event
{
[BsonId(IdGenerator = typeof(GuidGenerator) )]
[BsonRepresentation(BsonType.String)]
[BsonIgnoreIfDefault]
public Guid Id { get; set; }
// ... more properties and junk
}
And here is how we are performing the upsert operation:
// query to see if there are any pending operations
var keyMatchQuery = Query<Event>.In(r => r.Key, keyList);
var statusMatchQuery = Query<Event>.EQ(r => r.Status, "pending");
var query = Query.And(keyMatchQuery , statusMatchQuery );
var updateQuery = new UpdateBuilder();
var bson = request.ToBsonDocument();
foreach (var item in bson)
{
updateQuery.SetOnInsert(item.Name, item.Value);
}
var fields = Fields<Request>.Include(req => req.Id);
var args = new FindAndModifyArgs()
{
Fields = fields,
Query = query,
Update = updateQuery,
Upsert = true,
VersionReturned = FindAndModifyDocumentVersion.Modified
};
// Perform the upsert
var result = Collection.FindAndModify(args);
Doing it this way will generate the ID as an ObjectID rather than a GUID.
I can definitely get the behavior I want as a two step operation by performing a .FindOne first, and if it fails, doing a direct insert:
var existingItem = Collection.FindOneAs<Event>(query);
if (existingItem != null)
{
throw new PendingException(string.Format("Event already pending: id={0}", existingItem.Id));
}
var result = Collection.Insert(mongoRequest);
In this case, it correctly sets the GUID for the new item, but the operation is non-atomic. I was searching for a way to set the default ID generation mechanism at the driver level, and thought this would do it:
BsonSerializer.RegisterIdGenerator(typeof(Guid), GuidGenerator.Instance);
...but to no avail, and I assume that's because for the upsert, the ID field can't be included so there is no serialization happening and Mongo is doing all of the work. I also looked into implementing a convention, but that didn't make sense since there are separate generation mechanisms to handle that. Is there a different approach I should be looking at for this and/or am I just missing something?
I do realize that GUIDs are not always ideal in Mongo, but we are exploring using them due to compatibility with another system.
What's happening is that only the server knows whether the FindAndModify is going to end up being an upsert or not, and as currently written it is the server that is automatically generating the _id value, and the server can only assume that the _id value should be an ObjectId (the server knows nothing about your class declarations).
Here's a simplified example using the shell showing your scenario (minus all the C# code...):
> db.test.drop()
> db.test.find()
> var query = { x : 1 }
> var update = { $setOnInsert : { y : 2 } }
> db.test.findAndModify({ query: query, update : update, new : true, upsert : true })
{ "_id" : ObjectId("5346c3e8a8f26cfae50837d6"), "x" : 1, "y" : 2 }
> db.test.find()
{ "_id" : ObjectId("5346c3e8a8f26cfae50837d6"), "x" : 1, "y" : 2 }
>
We know this was an upsert because we ran it on an empty collection. Note that the server used the query as an initial template for the new document (that's where the "x" came from), applied the update specification (that's where the "y" came from), and because the document had no "_id" it generated a new ObjectId for it.
The trick is to generate the _id client side in case it turns out to be needed, but to put it in the update specification in such a way that it only applies if it's a new document. Here's the previous example using $setOnInsert for the _id:
> db.test.drop()
> db.test.find()
> var query = { x : 1 }
> var update = { $setOnInsert : { _id : "E3650127-9B23-4209-9053-1CD989AE62B9", y : 2 } }
> db.test.findAndModify({ query: query, update : update, new : true, upsert : true })
{ "_id" : "E3650127-9B23-4209-9053-1CD989AE62B9", "x" : 1, "y" : 2 }
> db.test.find()
{ "_id" : "E3650127-9B23-4209-9053-1CD989AE62B9", "x" : 1, "y" : 2 }
>
Now we see that the server used the _id we supplied instead of generating an ObjectId.
In terms of your C# code, simply add the following to your updateQuery:
updateQuery.SetOnInsert("_id", Guid.NewGuid().ToString());
You should consider renaming your updateQuery variable to updateSpecification (or just update) because technically it's not a query.
There's a catch though... this technique is only going to work against the current 2.6 version of the server. See: https://jira.mongodb.org/browse/SERVER-9958
You seem to be following the recommended practice for this, but possibly this is bypassed with "upserts" somehow. The general problem seems to be that the operation does not actually know which "class" it is actually dealing with and has no way of knowing that it needs to call the custom Id generator.
Any value that you pass in to MongoDB for the _id field will always be honored in place of generating the default ObjectID. Therefore if that field is included in the update "document" portion of the statement it will be used.
Probably the safest way to do this when expecting "upsert" behavior is to use the $setOnInsert modifier. Anything specified in here will only be set when an insert occurs from a related "upsert" operation. So in general terms:
db.collection.update(
{ "something": "matching" }
{
// Only on insert
"$setOnInsert": {
"_id": 123
},
// Always applied on update
"$set": {
"otherField": "value"
}
},
{ upsert: true }
)
So anything within the $set ( or other valid update operators ) will always be "updated" when the matching "query" condition is found. The $setOnInsert fields will be applied when the "insert" actually occurs due to no match. Naturally any literal conditions used in the query portion to "match" are also set so that future "upserts" will issue an "update" instead.
So as long as you structure your "update" BSON document to include your newly generated GUID in this way then you will always get the correct value in there.
Much of your code is on the right track, but you will need to invoke the method from your generator class and place value in the $setOnInsert portion of the statement, which you are already using, but just not including that _id value yet.

how to read back out the auto generated ID from mongo after insertion using official c# driver?

after you insert a new document into mongodb via the official c# driver, how to you immediately read back the generated _id so I can use it as a "foreign" key to other collections? i know in sql server i can immediately read back the identity column value for the newly inserted row, so i need the similar functionality in mongodb.
since the _id generated by mongo isn't an actual member of the object, assume you need to do something with the generic bsondocument?
You can do an upsert with the findAndModify command to achieve this same effect with less work than going through generating your own id's. (Why bother, there is a very good reason 10gen decided on the sceme that is used -- it enables easy sharding)
The findAndModify command lets you find or upsert (create if it doesn't exist) a document and return that same document.
The general form is as follows:
db.runCommand( { findAndModify : <collection>, <options> } )
You can read more about it here.
You would want to use the new in addition to the upsert option so that you get back the newly created object.
If you need the _id, you can generate it yourself and set it manually on the document.
In MongoDB, ids are (usually) generated on client side. And you can generate one yourself, using appropriate driver call, put in into document and it'll get saved.
I didn't work with C# driver, but Ruby driver does all the work for me.
ruby-1.9.3-p0 :027 > obj = coll.insert({'foo' => 'bar'})
=> BSON::ObjectId('4ef15e7f0ed4c00272000001')
ruby-1.9.3-p0 :030 > coll.find.to_a
=> [{"_id"=>BSON::ObjectId('4ef15e7f0ed4c00272000001'), "foo"=>"bar"}]
This is how I can make a new ID
ruby-1.9.3-p0 :039 > newid = BSON::ObjectId.new
=> BSON::ObjectId('4ef15f030ed4c00272000002')
ruby-1.9.3-p0 :040 > coll.insert({_id: newid, test: 'test'})
=> BSON::ObjectId('4ef15f030ed4c00272000002')
ruby-1.9.3-p0 :041 > coll.find.to_a
=> [{"_id"=>BSON::ObjectId('4ef15e7f0ed4c00272000001'), "foo"=>"bar"}, {"_id"=>BSON::ObjectId('4ef15f030ed4c00272000002'), "test"=>"test"}]
In most drivers the _id field is actually generated on the client side before going to the server. MongoDB does not use an "auto-increment" ID, so you can actually generate a random ID and tell the server "use this".
In C# the code looks like this:
var id = ObjectId.GenerateNewId();
So you can create a BSON document and simply save it:
var toSave = new BsonDocument {
{ "_id", ObjectId.GenerateNewId() },
{ "data", "my data" }
};
db.collection.Save(toSave);
However, by default, when you .Save() a document this will update the _id field. So you can generally just save the BSONDocument (or BSONSerializable class) and then read it back.
Note that there is a specification called DBRef that helps simplify the implementation of "Foreign Keys". The docs are here, in C# you will want to look at the DBRef class.
Like the other answers here say, IDs are assigned client-side. Something you can do is create a default value convention that generates a new ID during insert if it hasn't been set yet.
public class DefaultValueConvention : MongoDB.Bson.Serialization.Conventions.IDefaultValueConvention
{
public object GetDefaultValue(MemberInfo memberInfo)
{
var type = memberInfo.MemberType == MemberTypes.Property
? ((PropertyInfo) memberInfo).PropertyType
: ((FieldInfo) memberInfo).FieldType;
if (type.IsSubclassOf(typeof(ObjectId)))
return ObjectId.GenerateNewId();
else
return null;
}
}
And setup the driver to use this convention:
var profile = new ConventionProfile();
profile.SetDefaultValueConvention(new DefaultValueConvention());
BsonClassMap.RegisterConventions(profile, x => x.FullName.StartsWith("ConsoleApplication"));
So now you can create an object & persist it in 2 lines:
var animal = new Animal {Name = "Monkey", PercentDeviationFromHumans = 2.01};
db["animals"].Save(animal);
Actually, with the most recent driver you don't even need to set the default value convention, it already has this behavior OOTB. Regardless, conventions are underused in mongo.

Categories

Resources