Benchmarks for Inserting Documents Using C# for MongoDB

In previous articles, I explained how we could use C# and MongoDB to get records inserted. But we started this exploration into MongoDB, covering a lot of the basics, and I wanted to start looking into more interesting aspects of how we use these things together. As I was putting video content together about all of these topics, one thing that caught my attention was the generic methods vs. methods that operated on BsonDocument, and I was curious if the performance was any different. So, to start, I figured we’d look at C# MongoDB insert benchmarks and see if any interesting patterns stand out.

Considerations For These C# MongoDB Benchmarks

I was chatting with David Callan on Twitter the other day about benchmarks that he often posts on social media. It turns out there was another discussion floating around the Twitter-verse, where sync vs. async calls to databases were being debated. The proposal was that for very fast DB queries, the async variations are indeed slower.


Of course, this got the gears turning. I have been writing about and creating videos for MongoDB in C# all week and hinting at performance benchmarks coming. This coupled with my conversation with Dave AND the conversation I saw on Twitter made me think that I had to give this more attention. I speculated it might have something to do with Task vs ValueTask, but I didn’t have a lot of grounds for that.

However, I originally wanted to look into these benchmarks because I was curious if using BsonDocument compared to a dedicated DTO (whether it was a struct, class, or record variation) would have different performance characteristics.


That meant that I would want to ensure I covered a matrix across:

Synchronous
Asynchronous
Asynchronous with ValueTask
Struct
Class
Record Struct
Record Class
BsonDocument


And with that, I set off to go write some super simple benchmarks! Let’s check them out.

The C# MongoDB Insert Benchmarking Code

As with all benchmarking we do in C#, BenchmarkDotNet is our go-to tool! Make sure you start by installing the BenchmarkDotNet NuGet package. We’ll use this to ensure consistent setup, warmup, runs, and reporting for our MongoDB benchmarks.


Since we’re trying to reduce as many possible external factors as we can for these benchmarks, we’re going to use Testcontainers to run an instance of MongoDB in a Docker container. Of course, interacting with anything outside of our code directly can lead to inconsistencies and more room for error to show up in our results. However, this should help minimize things. You’ll want to get the Testcontainers.MongoDB NuGet package for this as well.


You can find all the relevant code on GitHub, but we’ll start with what our entry point looks like:

using BenchmarkDotNet.Running;

using System.Reflection;

BenchmarkRunner.Run(
Assembly.GetExecutingAssembly(),
args: args);


Nice and simple just to kick off the benchmarks. And the benchmarks are the most important part here:

using BenchmarkDotNet.Attributes;

using MongoDB.Bson;
using MongoDB.Driver;

using Testcontainers.MongoDb;

[MemoryDiagnoser]
//[ShortRunJob]
[MediumRunJob]
public class InsertBenchmarks
{
private MongoDbContainer? _container;
private MongoClient? _mongoClient;
private IMongoCollection<BsonDocument>? _collection;
private IMongoCollection<RecordStructDto>? _collectionRecordStruct;
private IMongoCollection<RecordClassDto>? _collectionRecordClass;
private IMongoCollection<StructDto>? _collectionStruct;
private IMongoCollection<ClassDto>? _collectionClass;

[GlobalSetup]
public async Task SetupAsync()
{
_container = new MongoDbBuilder()
.WithImage(“mongo:latest”)
.Build();
await _container.StartAsync();

_mongoClient = new MongoClient(_container.GetConnectionString());
var database = _mongoClient.GetDatabase(“test”);
_collection = database.GetCollection<BsonDocument>(“test”);
_collectionRecordStruct = database.GetCollection<RecordStructDto>(“test”);
_collectionRecordClass = database.GetCollection<RecordClassDto>(“test”);
_collectionStruct = database.GetCollection<StructDto>(“test”);
_collectionClass = database.GetCollection<ClassDto>(“test”);
}

[GlobalCleanup]
public async Task CleanupAsync()
{
await _container!.StopAsync();
}

[Benchmark]
public async Task InsertOneAsync_BsonDocument()
{
await _collection!.InsertOneAsync(new BsonDocument()
{
[“Name”] = “Nick Cosentino”,
});
}

[Benchmark]
public async ValueTask InsertOneAsyncValueTask_BsonDocument()
{
await _collection!.InsertOneAsync(new BsonDocument()
{
[“Name”] = “Nick Cosentino”,
});
}

[Benchmark]
public void InsertOne_BsonDocument()
{
_collection!.InsertOne(new BsonDocument()
{
[“Name”] = “Nick Cosentino”,
});
}

[Benchmark]
public async Task InsertOneAsync_RecordStruct()
{
await _collectionRecordStruct!.InsertOneAsync(new RecordStructDto(“Nick Cosentino”));
}

[Benchmark]
public async ValueTask InsertOneAsyncValueTask_RecordStruct()
{
await _collectionRecordStruct!.InsertOneAsync(new RecordStructDto(“Nick Cosentino”));
}

[Benchmark]
public void InsertOne_RecordStruct()
{
_collectionRecordStruct!.InsertOne(new RecordStructDto(“Nick Cosentino”));
}

[Benchmark]
public async Task InsertOneAsync_RecordClass()
{
await _collectionRecordClass!.InsertOneAsync(new RecordClassDto(“Nick Cosentino”));
}

[Benchmark]
public async ValueTask InsertOneAsyncValueTask_RecordClass()
{
await _collectionRecordClass!.InsertOneAsync(new RecordClassDto(“Nick Cosentino”));
}

[Benchmark]
public void InsertOne_RecordClass()
{
_collectionRecordClass!.InsertOne(new RecordClassDto(“Nick Cosentino”));
}

[Benchmark]
public async Task InsertOneAsync_Struct()
{
await _collectionStruct!.InsertOneAsync(new StructDto() { Name = “Nick Cosentino” });
}

[Benchmark]
public async ValueTask InsertOneAsyncValueTask_Struct()
{
await _collectionStruct!.InsertOneAsync(new StructDto() { Name = “Nick Cosentino” });
}

[Benchmark]
public void InsertOne_Struct()
{
_collectionStruct!.InsertOne(new StructDto() { Name = “Nick Cosentino” });
}

[Benchmark]
public async Task InsertOneAsync_Class()
{
await _collectionClass!.InsertOneAsync(new ClassDto() { Name = “Nick Cosentino” });
}

[Benchmark]
public async ValueTask InsertOneAsyncValueTask_Class()
{
await _collectionClass!.InsertOneAsync(new ClassDto() { Name = “Nick Cosentino” });
}

[Benchmark]
public void InsertOne_Class()
{
_collectionClass!.InsertOne(new ClassDto() { Name = “Nick Cosentino” });
}

private record struct RecordStructDto(string Name);

private record class RecordClassDto(string Name);

private struct StructDto
{
public string Name { get; set; }
}

private class ClassDto
{
public string Name { get; set; }
}
}


The benchmark code has as much as possible that we’re not interested in exercising pulled out into the GlobalSetup and GlobalCleanup marked methods.

Results For C# MongoDB Insert Benchmarks

Cue the drumroll! It’s time to look at our MongoDB benchmark results and do a little bit of an analysis:

Here are my takeaways from the benchmark data above:

All async variations used ~5KB more than the normal versions of the methods that we had to use.
There didn’t seem to be any difference in allocated memory for async vs async value task BUT Gen0 and Gen1 didn’t have any value for *some* of the ValueTask benchmarks — However, not for all of them. It almost looks like ValueTask combined with a struct data type for the insert results in Gen0 and Gen1 with no value, but plain old BsonDocument is an exception to this.
The fastest and lowest memory footprint seems to be InsertOneRecordClass, although the InsertOneBsonDocument is only a few microseconds off from this.
Async versions of the benchmarks seem slower than their normal versions across the board, as well.


This seems to be very much aligned with some of Twitter’s opening thoughts on Async operations! So, some hypotheses proved/disproved:

Async *is* overall worse for very fast DB operations
ValueTask doesn’t stand out as a consistent performance optimization in these situations
For single items, there’s no big difference in the memory footprint we’re seeing between any of these variations of the data types


It’ll be a good exercise to follow up with benchmarking, inserting many items into MongoDB from C#. I think we may start to see some of these variations stand out in different ways once we’re working with collections of items — But this is still a hypothesis that needs to be proven!

Wrapping Up C# MongoDB Insert Benchmarks

This was a simple investigation of insert benchmarks for MongoDB using C#. Overall, there were some surprises for me, but I still think there’s more investigation to be done when we’re working with multiple records at a time. I truly was a little bit surprised to see async be worse across the board since I figured that perhaps any type of IO would mask the performance impact of async overhead. But this was a fun experiment to try out, and there is more to come!


If you found this useful and you’re looking for more learning opportunities, consider subscribing to my free weekly software engineering newsletter and check out my free videos on YouTube!

Also published here.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.