MongoDB context implementation in C# as neat as Entity Framework

Mahdi Haji
4 min readFeb 4, 2022

--

If you have used MongoDB C#/.NET Driver you may notice that's not a very straightforward (read as clean) way to define configurations of the Collection. When I first started working with the driver following their getting started tutorial, I wondered why you need to define the storage configuration using multiple methods.
If you have experience with Entity Framework, you benefited from single place configuration in it using OnConfiguring method override. So I expected the same in MongoDB driver as well but here we have three definitions for what I call configuration as a whole:

1. Model configurations

You definitely need to define the way that your model would be saved (or mapped) to your Mongo database. So you need to use BsonClassMap like this:

BsonClassMap.RegisterClassMap<User>(cm => {
cm.AutoMap();
cm.SetIgnoreExtraElements(true);
cm.MapIdField(x => x.UserId);
});

2. Index definition:

If you want to define indexes for your collection you should do it like this:

var database = client.GetDatabase("MyApplication");
var collection = database.GetCollection<User>("Users");

var searchByIdentifier = new CreateIndexModel<User>(Builders<User>.IndexKeys
.Ascending(x => x.ContactDetails.Email)
.Ascending(x => x.ContactDetails.PhoneNumber));

var searchByUserId = new CreateIndexModel<User>(Builders<User>.IndexKeys
.Ascending(x => x.UserId));

collection.Indexes.CreateManyAsync(new []{ searchByIdentifier, searchByUserId});

3. Mapping Collection to the Model

When you are using a collection, you need to have the collection name:

var database = client.GetDatabase("MyApplication");
var collection = database.GetCollection<User>("Users");

I call all of these storage configurations and I thought we need to have a single place for all of these like what we have in EF.

So I thought we need a wrapper for doing it and it's here:

Solution: Use out MongoContext

What we followed in this project is the same way that we are familiar with in EF. please note that it's not an ORM but just a bunch of helpers that make the code neat and clean. So, let's play

Install ParkBee.MongoDb NuGet package in your data access/persistence layer:

Install-Package ParkBee.MongoDb

Create a context class that inherits from MongoContext, override OnConfiguring method and put all your configuration logic there.

public class MyApplicationContext : MongoContext  
{
public DbSet<User> Users { get; set; }
protected override async Task OnConfiguring()
{
await OptionsBuilder.Entity<User>(async entity =>
{
var usersCollection = entity.ToCollection("ApplicationUsers");
var searchByEmail = new CreateIndexModel<Permit>(Builders<User>.IndexKeys
.Ascending(u => u.Email));

await permitsCollection.Indexes.CreateOneAsync(searchByEmail);

entity.HasKey(p => p.UserId);
});
}
public MyApplicationContext(IMongoContextOptionsBuilder optionsBuilder) : base(optionsBuilder)
{
}
}

Install ParkBee.MongoDb.DependencyInjection NuGet package in your API / presentation layer:

Install-Package ParkBee.MongoDb.DependencyInjection

Register your mongo context in Startup.cs in ConfigureServices method

services.AddMongoContext<MyApplicationContext>(options =>
{
options.ConnectionString = "mongodb://localhost";
options.DatabaseName = "MyApplication";
});

Use the context injected in your controller:

public class UsersController : ControllerBase
{
private readonly MyApplicationContext _context;
public UsersController(MyApplicationContext context)
{
_context = context;
}

[HttpGet]
public async Task<IActionResult> GetUsers([FromQuery]SearchUsersRequest request, [FromQuery]QueryOptions queryOptions, CancellationToken cancellationToken)
{
var users = await _context.Users.ToListAsync(cancellationToken);

return Ok(users);
}
}

Explaining the context code

1. We introduce a property of type DbSet<User>:

public DbSet<User> Users { get; set; }

A DbSet is a wrapper class around IMongoCollection object and adds some features to it. You can use it as a collection object e.g.

await _context.Users.FindAsync(u => u.UserId == "something");

Or you can use it as IQueryable e.g.

await _context.Users.Where(u => u.UserId == "something").FirstOrDefaultAsync()

Note: for most of IQueryable operations you should reference MongoDB.Driver.Linq

And last but not least, DbSet provides functionality to get, update and delete documents by a key value. Please refer to HasKey section below

2. Configuring Users collection:

We configure each collection inside OptionsBuilder.Entity<> method.

Configuration Options

Inside OptionsBuilder.Entity<> we can use several methods to configure collections:

ToCollection

Use this method to map an entity (model class) to a collection. By default add properties of context class with DbSet<> type will be mapped with the same collection name as the property name.

So in our sample code, if we don’t use ToCollection method, this property will be mapped to a collection with name Users.

MapBson

This method is the place for putting mapping logic between MongoDb and model classes.

It provides the same way that used in BsonClassMap e.g.:

await OptionsBuilder.Entity<InternalUser>(async b =>
{
b.MapBson(cm =>
{
cm.AutoMap();
cm.SetDiscriminator(nameof(InternalUser));
cm.MapField("_products").SetElementName(nameof(InternalUser.Products));
cm.SetIgnoreExtraElements(true);
});
});

HasKey

This method maps a property of the model class to _id field in MongoDb and opens the possibility of usage of ByKey functionality of a DbSet:

FindByKey

It’s a simple way to get a single document by its configured key e.g.

var user = await _context.Users.FindByKey("userid");

UpdateByKey

Simply get a and update a single document e.g.

var user = await _context.Users.UpdateByKey("userid", Builders<User>.Update.Set(p => p.FirstName, "New Name"));

ReplaceByKey

Replace a single document fetched by it’s key e.g.

var user = await _context.Users.ReplaceByKey("userid", new User{ UserId = "userid", FirstName = "New Name"});

DeleteByKey

Delete a single document by its key e.g.

var user = await _context.Users.DeleteByKey("userid");

HasIndex

This method can be used to define indexes:

var searchByIdentifier = new CreateIndexModel<User>(Builders<User>.IndexKeys
.Ascending(x => x.ContactDetails.Email)
.Ascending(x => x.ContactDetails.PhoneNumber));

var searchByUserId = new CreateIndexModel<User>(Builders<User>.IndexKeys
.Ascending(x => x.UserId));
b.HasIndex(searchByIdentifier, searchByUserId);

And that's it

Please let me know if you find this useful or a waste of time

We are ParkBee.
A scale-up with a mission

ParkBee is the biggest digital parking platform making underused parking locations accessible to consumers. Our solution provides consumers with a large network of off-street parking spaces at attractive rates, whilst helping cities to make better use of under-utilised space and offering property owners with extra income using our smart and innovative platform.

Here we are using Microservices, DDD, EKS, MongoDB and so many other cool stuff and we always try to push the boundaries and make things better and better.

Curious if ParkBee is the place for you to grow? Have a look at our vacancies right now.

--

--

Mahdi Haji

I'm full stack .Net Angular developer in short. It's over than 15 years working as a developer and architect and I've used many technologies and frameworks