Best Practices When Working With MongoDB in .NET
Learn simple, proven MongoDB best practices in .NET: singleton client, connection pooling, indexes, projections, and safe writes explained for beginners.
Best Practices When Working With MongoDB in .NET
Imagine you run a small tiffin (lunchbox) service in your area. Every morning many customers call you at once. If you hire a brand new delivery boy for every single phone call, hire him, train him, and then fire him after one delivery, you will waste the whole day. Instead, you keep a small fixed team of delivery boys. When a call comes, a free boy picks it up. When he is done, he comes back and waits for the next call.
That fixed team is exactly how a MongoDB client works in .NET. You make it once. It keeps a small team of ready connections. Every request borrows one and returns it. If you create a new client for every request, you are hiring and firing delivery boys all day. Your app becomes slow and tired.
This article shows you the simple habits that keep MongoDB fast, safe, and easy in your .NET apps. We will go slow. Short sentences. Real examples.
The big picture first
Before rules, see the shape of things. A .NET app talks to MongoDB through a few clear layers.
You write code. Your code uses one IMongoClient. The client holds a pool of connections. The pool talks to the MongoDB server. The server stores your data in collections (like tables, but flexible).
Keep this picture in your head. Most mistakes happen when people break this flow, usually by making too many clients.
Rule 1: One client for the whole app
This is the most important rule. The MongoClient is the owner of the connection pool. Make it once. Share it everywhere. Register it as a singleton.
A singleton means: one single instance for the entire life of the app.
Here is the clean way to do it in a modern .NET app.
using MongoDB.Driver;
var builder = WebApplication.CreateBuilder(args);
// Read the connection string from configuration
var connectionString = builder.Configuration
.GetConnectionString("Mongo")!;
// ONE client for the whole app -> register as singleton
builder.Services.AddSingleton<IMongoClient>(_ =>
new MongoClient(connectionString));
// A handy way to get the database from the single client
builder.Services.AddScoped(sp =>
{
var client = sp.GetRequiredService<IMongoClient>();
return client.GetDatabase("tiffin_service");
});
var app = builder.Build();Notice the pattern. The client is a singleton (made once). The database handle is cheap, so getting it per request is fine.
Why does this matter so much? Look at the difference.
| Approach | What happens | Result |
|---|---|---|
| New client per request | New pool every time, sockets pile up | Slow, may crash under load |
| One singleton client | Pool is reused by everyone | Fast and stable |
| Static client field | Works, but harder to test | Okay, but DI is cleaner |
The MongoDB docs are very clear on this: create the client once and store it in a global place or your IoC container with a singleton lifetime.
Rule 2: Understand the connection pool
The pool is the team of delivery boys. You can size the team using settings. The driver lets you set these through MongoClientSettings.
| Setting | Meaning | Default |
|---|---|---|
| MaxConnectionPoolSize | Most connections allowed | 100 |
| MinConnectionPoolSize | Connections kept warm and ready | 0 |
| WaitQueueTimeout | How long a request waits for a free connection | 2 minutes |
| MaxConnectionIdleTime | When an idle connection is closed | 10 minutes |
Most apps are happy with the defaults. But if you have many users at the same time, you can tune the size.
var settings = MongoClientSettings.FromConnectionString(connectionString);
// Size the pool for your real traffic
settings.MaxConnectionPoolSize = 200;
settings.MinConnectionPoolSize = 10; // keep some ready
settings.ApplicationName = "TiffinApi"; // shows up in server logs
var client = new MongoClient(settings);Setting MinConnectionPoolSize keeps a few connections warm. So the first request in the morning does not pay the cost of opening a fresh connection. Setting ApplicationName helps you spot your app in MongoDB server logs. That is a small habit that saves big headaches later.
Here is how borrowing a connection works step by step.
Borrowing a connection from the pool
Steps
Request
Code needs the DB
Check out
Take a free connection
Run query
Talk to MongoDB
Return
Give it back to pool
If no connection is free, the request waits in a queue. If it waits longer than WaitQueueTimeout, it fails. That is a signal that your pool is too small for your traffic, or your queries are too slow.
Rule 3: Use strong types, not loose documents
MongoDB stores flexible documents. But in .NET, you should still map them to real C# classes. This gives you safety and clear code.
public class TiffinOrder
{
public ObjectId Id { get; set; }
public string CustomerName { get; set; } = "";
public string MealType { get; set; } = ""; // "veg" or "non-veg"
public int Quantity { get; set; }
public DateTime DeliveryDate { get; set; }
public bool IsPaid { get; set; }
}Now you can get a typed collection and work safely.
public class OrderRepository
{
private readonly IMongoCollection<TiffinOrder> _orders;
public OrderRepository(IMongoDatabase database)
{
_orders = database.GetCollection<TiffinOrder>("orders");
}
public async Task<List<TiffinOrder>> GetForDateAsync(DateTime date)
{
var filter = Builders<TiffinOrder>.Filter
.Eq(o => o.DeliveryDate, date.Date);
return await _orders.Find(filter).ToListAsync();
}
}Use the builder style (Builders<T>.Filter) instead of raw strings. The compiler checks your field names. If you rename DeliveryDate, the code breaks at build time, not at midnight in production.
Rule 4: Indexes are not optional
This is where flexible databases trick beginners. People think "MongoDB is schema-less, so it is fast by magic." That is false. Without an index, MongoDB reads every document to find a match. With ten documents, that is fine. With ten million, your app freezes.
An index is like the index at the back of a textbook. Instead of reading all pages, you jump straight to the right page.
Create indexes for fields you search or sort on often. Do it once at startup.
public static async Task EnsureIndexesAsync(IMongoCollection<TiffinOrder> orders)
{
var byDate = new CreateIndexModel<TiffinOrder>(
Builders<TiffinOrder>.IndexKeys.Ascending(o => o.DeliveryDate));
var byCustomer = new CreateIndexModel<TiffinOrder>(
Builders<TiffinOrder>.IndexKeys.Ascending(o => o.CustomerName));
await orders.Indexes.CreateManyAsync(new[] { byDate, byCustomer });
}A simple rule: if you filter by it or sort by it, it probably needs an index. But do not add an index to every field. Each index costs memory and slows down writes. Pick the fields that matter.
Rule 5: Ask only for what you need (projections)
When you only need a customer name and meal type, do not pull the whole document. Pulling extra fields wastes network and memory. This is called using a projection.
var names = await _orders
.Find(o => o.DeliveryDate == DateTime.Today)
.Project(o => new { o.CustomerName, o.MealType })
.ToListAsync();Think of it like a thali. If you only want rice and dal, do not ask for the full twenty-item meal and throw most of it away.
The same idea applies to paging. Never load a whole collection into memory. Use Skip and Limit.
var page = await _orders
.Find(_ => true)
.SortByDescending(o => o.DeliveryDate)
.Skip(pageNumber * pageSize)
.Limit(pageSize)
.ToListAsync();Rule 6: Always go async
MongoDB calls go over the network. Network calls take time. While waiting, your thread should be free to serve other users. So use the Async methods and await them. Never block with .Result or .Wait(). Blocking ties up threads and can deadlock your app under load.
Async keeps the app responsive
Steps
Start call
Send query to MongoDB
Await
Do not block the thread
Thread freed
Serve other users
Result back
Resume your method
Rule 7: Write safely and handle errors
Writes can fail. The network can drop. A duplicate key can clash. Wrap writes and react sensibly.
public async Task<bool> AddOrderAsync(TiffinOrder order)
{
try
{
await _orders.InsertOneAsync(order);
return true;
}
catch (MongoWriteException ex)
when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
{
// A unique index blocked a repeated order. Handle gracefully.
return false;
}
}For updates, prefer targeted updates over reading the whole document, changing it in C#, and writing it all back. A targeted update changes only the field you want.
var filter = Builders<TiffinOrder>.Filter.Eq(o => o.Id, id);
var update = Builders<TiffinOrder>.Update.Set(o => o.IsPaid, true);
await _orders.UpdateOneAsync(filter, update);This is faster and avoids overwriting changes that someone else made at the same time.
Rule 8: Keep secrets out of code
Never paste your connection string with the password right into your C# files. Anyone reading the repo would see it. Put it in configuration, user secrets during development, or environment variables and a secret store in production.
// appsettings.json holds a placeholder; the real value comes from
// environment variables or a secret manager at runtime.
var connectionString = builder.Configuration.GetConnectionString("Mongo");This keeps your database safe and lets you change the password without touching code.
A quick look at a clean setup
Here is how the pieces fit together as one tidy flow, from app start to a running query.
This is the whole story in one diagram. One client. Indexes ready. A repository that asks clear questions. MongoDB answers fast.
Common mistakes to avoid
Let me list the traps that catch most beginners, so you can skip the pain.
- Making a new
MongoClientinside a controller or per request. This kills performance. - Forgetting indexes, then blaming MongoDB for being slow.
- Loading entire collections into memory with no paging.
- Using
.Resultand.Wait()instead ofawait. - Storing the connection string with the password in source control.
- Reading a whole document just to change one field.
Each of these is easy to fix once you know it. And now you know it.
Quick recap
- Treat the client like a fixed team of delivery boys. Make it once, share it everywhere, register it as a singleton.
- The client owns a connection pool. Tune
MaxConnectionPoolSizeandMinConnectionPoolSizeonly when your traffic needs it. - Map documents to strong C# types and use the builder filters so the compiler protects you.
- Indexes are not optional. Index the fields you search or sort on. Do not index everything.
- Use projections and paging to ask only for the data you actually need.
- Always go async and never block with
.Resultor.Wait(). - Handle write errors and prefer targeted updates over full document rewrites.
- Keep your connection string and password out of code and in safe configuration.
References and further reading
- MongoDB C#/.NET Driver Documentation — the official, always-current guide.
- Connection Pools - C#/.NET Driver — pool settings and defaults explained.
- Specify Connection Options - C#/.NET Driver — full list of client settings.
- Best Practices When Working With MongoDB in .NET (Anton Martyniuk) — a well-known community write-up.
- How to Use Connection Pooling with the MongoDB .NET Driver — a friendly community walkthrough.
Related Posts
Getting Started With MongoDB in EF Core: A Beginner's Guide
A friendly beginner guide to using MongoDB with EF Core in .NET. Learn setup, DbContext, UseMongoDB, CRUD, mapping, and the limits you must know.
Mastering Exception Handling in C#: A Comprehensive Guide
Learn C# exception handling the friendly way: try, catch, finally, custom exceptions, filters, throw vs throw ex, and real best practices for .NET 10.
5 Awesome C# Refactoring Tips to Write Cleaner Code
Learn 5 simple C# refactoring tips with examples in modern C# 14. Make your code cleaner, safer, and easier to read using guard clauses and more.
Top 15 Mistakes .NET Developers Make and How to Avoid Common Pitfalls
Learn the 15 most common mistakes .NET developers make with async, EF Core, HttpClient, and memory, plus simple fixes you can use today.
Understanding Change Tracking for Better Performance in EF Core
Learn how EF Core change tracking works, the entity states it uses, and simple tricks like AsNoTracking to make your .NET apps faster.
Calling Views, Stored Procedures and Functions in EF Core
A friendly, beginner guide to calling database views, stored procedures, and functions in EF Core with FromSql, SqlQuery, ExecuteSql, and ToView.