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 CSharp MongoDB Benchmarks
I was chatting with David Callan on Twitter the other day about benchmarks that he often posts on social media. Turns out there was another discussion floating around the Twitter-verse where it was being debated about sync vs async calls to databases. 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 CSharp MongoDB Insert Benchmarking Code
As with all benchmarking that we do in C#, BenchmarkDotNet is our go-to tool! Make sure you start by getting the BenchmarkDotNet NuGet package installed. We’ll be using this to ensure we have 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 be using Testcontainers to run an instance of MongoDB in a Docker container. Of course, by interacting with anything outside of our code directly there are opportunities for 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 CSharp 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 InsertOne_RecordClass, although the InsertOne_BsonDocument 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 does seem to be very much aligned with what some of the opening thoughts were from Twitter for 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 CSharp MongoDB Insert Benchmarks
This was a simple investigation of insert benchmarks for MongoDB using C#. Overall, 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 with 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! Meet other like-minded software engineers and join my Discord community!
Want More Dev Leader Content?
- Follow along on this platform if you haven’t already!
- Subscribe to my free weekly software engineering and dotnet-focused newsletter. I include exclusive articles and early access to videos: SUBSCRIBE FOR FREE
- Looking for courses? Check out my offerings: VIEW COURSES
- E-Books & other resources: VIEW RESOURCES
- Watch hundreds of full-length videos on my YouTube channel: VISIT CHANNEL
- Visit my website for hundreds of articles on various software engineering topics (including code snippets): VISIT WEBSITE
- Check out the repository with many code examples from my articles and videos on GitHub: VIEW REPOSITORY
Top comments (1)
Hi Dev Leader,
Your tips are very useful
Thanks for sharing