HomeNikola Knezevic

In this article

Banner

Getting Started with MongoDB in ASP.NET Core

30 Jul 2026
6 min

Sponsor Newsletter

Not every application fits a relational database well. Sometimes your data is document shaped, evolves often or does not need rigid schemas and joins.

In those cases, forcing everything into tables and migrations can slow you down more than it helps.

That's where MongoDB comes in. In this post, we'll wire up the official MongoDB.Driver.

What is MongoDB?

MongoDB is a document database. Instead of rows in tables, you store BSON documents in collections.

Documents map naturally to .NET objects. Related data can live together in one document, which often means fewer round trips and simpler reads.

For .NET, the official client is MongoDB.Driver. It gives you typed collections, filters, projections and async APIs that fit ASP.NET Core well.

Without further ado, let's get started.

Getting Started

Install the driver package:

shell
dotnet add package MongoDB.Driver

You'll also need a running MongoDB instance. In the sample project I use Docker Compose with MongoDB and mongo-express, but any local or cloud cluster works.

Configuration

Keep connection details in configuration. A simple appsettings.json section looks like this:

json
{
  "MongoDb": {
    "ConnectionString": "mongodb://admin:admin@localhost:27017",
    "Database": "Products",
    "Collection": "products"
  }
}

Bind that section to a strongly typed options class:

csharp
public class MongoDbOptions
{
    public string ConnectionString { get; set; }

    public string Database { get; set; }

    public string Collection { get; set; }
}

For more details on binding and validating settings, check out my previous blog post on the Options Pattern.

Registering MongoClient

Register IMongoClient as a singleton. The client is thread-safe and meant to be reused for the lifetime of the app:

csharp
public static IServiceCollection AddDatabase(
    this IServiceCollection services,
    IConfiguration configuration)
{
    services.Configure<MongoDbOptions>(
        configuration.GetSection("MongoDb"));

    services.AddSingleton<IMongoClient>(serviceProvider =>
    {
        var options = serviceProvider
            .GetRequiredService<IOptions<MongoDbOptions>>()
            .Value;

        return new MongoClient(options.ConnectionString);
    });

    return services;
}

From the client you resolve a database, then a typed collection. Handlers will do that with the options you just configured.

Modeling Documents

Here's a simple Product entity mapped for MongoDB:

csharp
public sealed class Product
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    public string Id { get; set; }

    [BsonDateTimeOptions(Kind = DateTimeKind.Utc)]
    public DateTime CreatedAt { get; set; }

    [BsonDateTimeOptions(Kind = DateTimeKind.Utc)]
    public DateTime? ModifiedAt { get; set; }

    public string Name { get; set; }

    public string Description { get; set; }

    public decimal Price { get; set; }

    public Product(
        string id,
        DateTime createdAt,
        string name,
        string description,
        decimal price)
    {
        Id = id;
        CreatedAt = createdAt;
        Name = name;
        Description = description;
        Price = price;
        ModifiedAt = null;
    }
}

BsonId marks the document identifier. BsonRepresentation(BsonType.ObjectId) stores it as a MongoDB ObjectId while you keep a string in C#.

BsonDateTimeOptions keeps timestamps in UTC, which is what you usually want for APIs.

Creating Documents

To insert a document, resolve the collection and call InsertOneAsync:

csharp
internal sealed class Handler : IRequestHandler<Command, Result<string>>
{
    private readonly IMongoCollection<Product> _collection;

    public Handler(
        IMongoClient client,
        IOptions<MongoDbOptions> mongoDbOptions)
    {
        var database = client.GetDatabase(mongoDbOptions.Value.Database);
        _collection = database.GetCollection<Product>(
            mongoDbOptions.Value.Collection);
    }

    public async Task<Result<string>> Handle(
        Command request,
        CancellationToken cancellationToken)
    {
        var product = new Product(
            ObjectId.GenerateNewId().ToString(),
            DateTime.UtcNow,
            request.Name,
            request.Description,
            request.Price);

        await _collection.InsertOneAsync(product, cancellationToken);

        return Result.Success(product.Id);
    }
}

ObjectId.GenerateNewId() creates a new MongoDB id before insert. The handler returns that id to the caller.

Reading Documents

Reads use Builders<T>.Filter for predicates and Project when you only need part of the document.

Fetching a single product by id:

csharp
public async Task<Result<ProductResponse>> Handle(
    Query request,
    CancellationToken cancellationToken)
{
    var filter = Builders<Product>.Filter
        .Eq(x => x.Id, request.Id);

    var product = await _collection
        .Find(filter)
        .Project(x => new ProductResponse(
            x.Id,
            x.Name,
            x.Description,
            x.Price))
        .FirstOrDefaultAsync(cancellationToken);

    return product is null
        ? Result.NotFound()
        : Result.Success(product);
}

Listing everything is just as straightforward. Use an empty filter:

csharp
public async Task<IEnumerable<ProductResponse?>> Handle(
    Query request,
    CancellationToken cancellationToken) =>
    await _collection
        .Find(FilterDefinition<Product>.Empty)
        .Project(p => new ProductResponse(
            p.Id,
            p.Name,
            p.Description,
            p.Price))
        .ToListAsync(cancellationToken);

Projection keeps responses lean. You only pull the fields your API actually returns.

Updating Documents

Partial updates use Builders<T>.Update with UpdateOneAsync:

csharp
public async Task<Result> Handle(
    Command request,
    CancellationToken cancellationToken)
{
    var filter = Builders<Product>.Filter
        .Eq(p => p.Id, request.Id);

    var update = Builders<Product>.Update
        .Set(p => p.Name, request.Name)
        .Set(p => p.Description, request.Description)
        .Set(p => p.Price, request.Price);

    var result = await _collection.UpdateOneAsync(
        filter,
        update,
        cancellationToken: cancellationToken);

    return result.MatchedCount == 0
        ? Result.NotFound()
        : Result.Success();
}

You do not need to load the full document first. Set only the fields that change and check MatchedCount to see if anything matched.

Deleting Documents

Deletes follow the same pattern with DeleteOneAsync:

csharp
public async Task<Result> Handle(
    Command request,
    CancellationToken cancellationToken)
{
    var deleteResult = await _collection.DeleteOneAsync(
        x => x.Id == request.Id,
        cancellationToken);

    return deleteResult.DeletedCount == 0
        ? Result.NotFound()
        : Result.Success();
}

If nothing was deleted, treat it as not found. Otherwise return success.

Conclusion

Getting started with MongoDB in ASP.NET Core is mostly package setup, options binding and a singleton IMongoClient.

From there, typed collections plus filters, projections and update builders cover everyday CRUD without an ORM.

The sample behind this post also organizes Minimal API endpoints with MediatR. If you want that structure, I've written a dedicated blog post on organizing Minimal APIs that you can check out here if you're interested.

If you want to check out examples I created, you can find the source code here:

Source Code

I hope you enjoyed it, subscribe and get a notification when a new blog is up!

Subscribe

Stay tuned for valuable insights every Thursday morning.