Skip to main content
SEMastery
Data Accessintermediate

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.

11 min readUpdated December 1, 2025

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.

How a .NET app talks to MongoDB

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.

ApproachWhat happensResult
New client per requestNew pool every time, sockets pile upSlow, may crash under load
One singleton clientPool is reused by everyoneFast and stable
Static client fieldWorks, but harder to testOkay, 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.

SettingMeaningDefault
MaxConnectionPoolSizeMost connections allowed100
MinConnectionPoolSizeConnections kept warm and ready0
WaitQueueTimeoutHow long a request waits for a free connection2 minutes
MaxConnectionIdleTimeWhen an idle connection is closed10 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

Request
Check out
Run query
Return

Steps

1

Request

Code needs the DB

2

Check out

Take a free connection

3

Run query

Talk to MongoDB

4

Return

Give it back to pool

A request checks out a connection, uses it, then returns it

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.

Searching without an index vs with an index

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

Start call
Await
Thread freed
Result back

Steps

1

Start call

Send query to MongoDB

2

Await

Do not block the thread

3

Thread freed

Serve other users

4

Result back

Resume your method

The thread is freed while waiting for MongoDB

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.

A clean MongoDB setup in .NET from startup to 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 MongoClient inside 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 .Result and .Wait() instead of await.
  • 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 MaxConnectionPoolSize and MinConnectionPoolSize only 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 .Result or .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

Related Posts