DEV Community

Mauro Petrini 👨‍💻
Mauro Petrini 👨‍💻

Posted on • Edited on

A guide to bulk write operations in MongoDB with C#


Welcome! Spanish articles on LinkedIn. You can follow me on Twitter for news.


Table of Contents

  1. MongoDB and C# intro
  2. Insert operations
  3. Update operations
  4. Delete operations
  5. Put it all together

MongoDB and C# introduction

🚀 MongoDB provides the ability to perform bulk insert, update, and delete operations.

In the MongoDB C# Driver,, we can use the BulkWriteAsync() method that supports the following write operations:

  • InsertOne
  • UpdateOne
  • UpdateMany
  • DeleteOne
  • DeleteMany

We are going to show how to use these methods with the MongoDB's C# driver.

I'll work through this sample in MongoDB Atlas, the global cloud database service offered by MongoDB.

You can create your cluster in minutes, completely free here

Let's begin

I create a user collection with the following fields.

  • _id
  • name
  • email
  • createdAt
  • isBlocked

YES! There is no password! It's not the scope of this post.

    public class User
    {
        [BsonId] public ObjectId _id { get; set; }
        public BsonString name { get; set; }
        public BsonString email { get; set; }
        public BsonDateTime createdAt { get; set; }
        public BsonBoolean isBlocked { get; set; }
    }

Insert operations

BulkWriteAsync method

This method is responsible for executing the bulk operations. BulkWriteAsync takes a variable number (list) of WriteModel instances.

We set our User model as a generic type parameter to the WriteModel class.

    var listWrites = new List<WriteModel<User>>();

Creating a fake user dataset

Then, we are going to create a fake dataset of 1000 new users and add this to the list of WriteModel type.

    var totalNewUsers = 1000;

    for (int i = 0; i < totalNewUsers; i++)
    {
        var newUser = new User
        {
            name = $"customName-{i}",
            email = $"customEmail-{i}@domain{i}.com",
            createdAt = DateTime.Now,
            isBlocked = false
        };

        listWrites.Add(new InsertOneModel<User>(newUser));
    }

Pay attention to the InsertOneModel object. We are telling MongoDB that it's an insert operation.

Execute batch insert operation

Finally, we get the user collection and execute the bulk insert.

    var userCollection = db.GetCollection<User>("users");
    var resultWrites = await userCollection.BulkWriteAsync(listWrites);

    Console.WriteLine($"OK?: {resultWrites.IsAcknowledged} - Inserted Count: {resultWrites.InsertedCount}");

By default, MongoDB executes the bulk insert with an ordered approach. That's means perform the operations serially. If an error occurs of one of the insert operation, MongoDB ends the bulk insert. If you want MongoDB to continue with the bulk insert, you need to specify the unordered approach passing a BulkWriteOptions object:

    var resultWrites = await userCollection.BulkWriteAsync(listWrites, new BulkWriteOptions
    {
        IsOrdered = false
    });

Complete Code

    public static async Task BulkInsertMongoDb()
    {
        IMongoDatabase db = _client.GetDatabase("sample_blog");

        var listWrites = new List<WriteModel<User>>();
        var totalNewUsers = 1000;

        for (int i = 0; i < totalNewUsers; i++)
        {
            var newUser = new User
            {
                name = $"customName-{i}",
                email = $"customEmail-{i}@domain{i}.com",
                createdAt = DateTime.Now,
                isBlocked = false
            };

            listWrites.Add(new InsertOneModel<User>(newUser));
        }

        var userCollection = db.GetCollection<User>("users");
        var resultWrites = await userCollection.BulkWriteAsync(listWrites);

        Console.WriteLine($"OK?: {resultWrites.IsAcknowledged} - Inserted Count: {resultWrites.InsertedCount}");
    }

Update operations

After insert 1,000 new users, we are going to update some of them!

UpdateOne

This class (UpdateOneModel) provides us a way to update only one document that matches a specific condition.

We need to set up a filter definition (condition) and the update definition (what fields are going to update)

    var filterDefinition = Builders<User>.Filter.Eq(p => p.email, "customEmail-0@domain0.com");
    var updateDefinition = Builders<User>.Update.Set(p => p.isBlocked, true);

Then, add these definitions to our UpdateOneModel instance.

    listWrites.Add(new UpdateOneModel<User>(filterDefinition, updateDefinition));

Complete code for UpdateOne

    public static async Task BulkUpdateOneMongoDb()
    {
        IMongoDatabase db = _client.GetDatabase("sample_blog");
        var userCollection = db.GetCollection<User>("users");

        var listWrites = new List<WriteModel<User>>();

        var filterDefinition = Builders<User>.Filter.Eq(p => p.email, "customEmail-0@domain0.com");
        var updateDefinition = Builders<User>.Update.Set(p => p.isBlocked, true);

        listWrites.Add(new UpdateOneModel<User>(filterDefinition, updateDefinition));

        await userCollection.BulkWriteAsync(listWrites);
    }

UpdateMany

This class (UpdateManyModel) allows us to update multiple documents that match a specific condition.

As we did with UpdateOne, we need to set up a filter definition and the updated definition.

    var filterDefinition = Builders<User>.Filter.Eq(p => p.isBlocked, false);
    var updateDefinition = Builders<User>.Update.Set(p => p.isBlocked, true);

In this case, we are going to block all users that there aren't blocked yet.

Complete code for UpdateMany

    public static async Task BulkUpdateManyMongoDb()
    {
        IMongoDatabase db = _client.GetDatabase("sample_blog");
        var userCollection = db.GetCollection<User>("users");

        var listWrites = new List<WriteModel<User>>();

        var filterDefinition = Builders<User>.Filter.Eq(p => p.isBlocked, false);
        var updateDefinition = Builders<User>.Update.Set(p => p.isBlocked, true);

        listWrites.Add(new UpdateManyModel<User>(filterDefinition, updateDefinition));

        await userCollection.BulkWriteAsync(listWrites);
    }

UpdateOne and UpdateMany

As we saw, the implementations of these methods are the same. The main difference is that if the condition matches multiple documents, UpdateOne will update the first matching document only while UpdateMany updates all documents.

Delete operations

It's very similar to update operations, but in this case, we are going to delete some documents in our user collection.

DeleteOne

This class (DeleteOneModel) provides us a way to delete only one document that matches a specific condition

We need to set up a filter definition (condition)

    var filterDefinition = Builders<User>.Filter.Eq(p => p.email, "customEmail-0@domain0.com");

In this case, we are going to delete the first document that matches an email equals customEmail-0@domain0.com.

Complete code for DeleteOne

    public static async Task BulkDeleteOneMongoDb()
    {
        IMongoDatabase db = _client.GetDatabase("sample_blog");
        var userCollection = db.GetCollection<User>("users");

        var listWrites = new List<WriteModel<User>>();

        var filterDefinition = Builders<User>.Filter.Eq(p => p.email, "customEmail-0@domain0.com");
        listWrites.Add(new DeleteOneModel<User>(filterDefinition));

        await userCollection.BulkWriteAsync(listWrites);
    }

DeleteMany

This class (DeleteManyModel) allows us to delete multiple documents that match a specific condition.

As we did with DeleteOne, we need to set up a filter definition.

    var filterDefinition = Builders<User>.Filter.Eq(p => p.isBlocked, true);

In this case, we are going to delete all users that there are blocked.

Complete code for DeleteMany

    public static async Task BulkDeleteManyMongoDb()
    {
        IMongoDatabase db = _client.GetDatabase("sample_blog");
        var userCollection = db.GetCollection<User>("users");

        var listWrites = new List<WriteModel<User>>();

        var filterDefinition = Builders<User>.Filter.Eq(p => p.isBlocked, true);
        listWrites.Add(new DeleteManyModel<User>(filterDefinition));

        await userCollection.BulkWriteAsync(listWrites);
    }

DeleteOne and DeleteMany

As we said with UpdateOne and UpdateMany, the implementations of these methods are the same. The main difference is that if the condition matches multiple documents, DeleteOne will delete the first matching document only while DeleteMany deletes all documents.

Put it all together

MongoDB allows us to execute all of this together 🤩
The following sample does not make sense, but I want to show that it is possible!

We are going to group all previous operations in the same bulk write operations so that we will create 1,000 new users, then execute UpdateOne, UpdateMany, DeleteOne, and DeleteMany operations in the same bulk in order.

    public static async Task BulkAllTogetherMongoDb()
    {
        IMongoDatabase db = _client.GetDatabase("sample_blog");

        var listWrites = new List<WriteModel<User>>();
        var totalNewUsers = 1000;

        //InsertOne
        for (int i = 0; i < totalNewUsers; i++)
        {
            var newUser = new User
            {
                name = $"customName-{i}",
                email = $"customEmail-{i}@domain{i}.com",
                createdAt = DateTime.Now,
                isBlocked = false
            };

            listWrites.Add(new InsertOneModel<User>(newUser));
        }

        //UpdateOne
        var filterDefinition = Builders<User>.Filter.Eq(p => p.email, "customEmail-0@domain0.com");
        var updateDefinition = Builders<User>.Update.Set(p => p.isBlocked, true);

        listWrites.Add(new UpdateOneModel<User>(filterDefinition, updateDefinition));

        //UpdateMany
        var filterDefinitionUpdateMany = Builders<User>.Filter.Eq(p => p.isBlocked, false);
        var updateDefinitionUpdateMany = Builders<User>.Update.Set(p => p.isBlocked, true);

        listWrites.Add(new UpdateManyModel<User>(filterDefinitionUpdateMany, updateDefinitionUpdateMany));

        //DeleteOne
        var filterDefinitionDeleteOne = Builders<User>.Filter.Eq(p => p.email, "customEmail-0@domain0.com");
        listWrites.Add(new DeleteOneModel<User>(filterDefinitionDeleteOne));

        //DeleteMany
        var filterDefinitionDeleteMany = Builders<User>.Filter.Eq(p => p.isBlocked, true);
        listWrites.Add(new DeleteManyModel<User>(filterDefinitionDeleteMany));

        var userCollection = db.GetCollection<User>("users");
        var resultWrites = await userCollection.BulkWriteAsync(listWrites);

        Console.WriteLine($"OK?: {resultWrites.IsAcknowledged} - Inserted Count: {resultWrites.InsertedCount}");
        Console.WriteLine($"Updated Count: {resultWrites.ModifiedCount}");
        Console.WriteLine($"Deleted Count: {resultWrites.DeletedCount}");
    }

Console output:

OK?: True - Inserted Count: 1000
Updated Count: 1000
Deleted Count: 1000

I hope you enjoyed the post!

Top comments (2)

Collapse
 
sbittis profile image
Sebastian Bittis • Edited

Thanks, useful post! 👏
Would be great to extend it by error handling and transactions as you often need to roll back the whole bulk in case of any error and return the operation in the bulk which caused the error.

A transaction can be created by using a session:

using var session = await _client.StartSessionAsync();
session.StartTransaction();
...
await session.CommitTransactionAsync();
Enter fullscreen mode Exit fullscreen mode

Error handling can be done catching the MongoBulkWriteException:

try
{
    ...
}
catch (MongoBulkWriteException exception)
{
    await session.AbortTransactionAsync();
    foreach (var writeError in exception.WriteErrors)
    {
        // work on writeError
    }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
vekzdran profile image
Vedran Mandić

Very useful article, thank you!