Getting Started With MongoDB and C#

MongoDB is a document oriented database that, despite being associated with unstructured data, can pair very nicely with C#. The MongoDB driver for C# does a very good job of emulating most of the functionality in Microsoft’s Entity Framework and mapping potentially unstructured data with strongly typed POCOs. It strives to match up the document model of MongoDB with your C# classes in a frictionless way.

I am going to focus on an implementation of MongoDB in an application that readily supports Dependency Injection and a single shared MongoClient throughout the application. This should be considered a way of implementing it, your project requirements may vary and you should use a model that suits you best. Even if you don’t follow this model precisely, there is functionality in the C# driver, “under the hood” that will help keep you from straying too far into “not recommended” territory.

Lets start by looking at how we start using the Mongo Driver in C#. First lets briefly discuss what is available on NuGet from MongoDB.

  • MongoDB.Driver
  • MongoDB.Bson
  • MongoDB.Driver.Core
  • MongoDB.Driver.GridFS
  • mongocsharpdriver

The MongoDB.Driver package is the currently recommended package to install to use the MongoDB Driver. The other 3 (Bson, Driver.Core, and Driver.GridFS) provide extra functionality that has been abstracted out of MongoDB.Driver for sharing purposes. The mongocsharpdriver is the “legacy” driver, but this (currently) really only applies to the API. It is capable of working with all modern versions of MongoDB, but it is set to be deprecated at some point in the future.

So lets start by creating a new ASP.NET Core Application and adding the driver to the project with

1
2
3
4
mkdir webApiSample
cd webApiSample
dotnet new webapi
dotnet add package MongoDB.Driver --version 2.7.0

Now lets take a look at how to connect to a MongoDB deployment (in this case a Standalone running locally) from code. The main entry point into MongoDB from C# is via the MongoClient type. Which has several different constructors, we are going to focus mainly on the one that takes a Mongo URI (or “connection string”). The MongoClient object provides access to databases (which in turn provide access to collections). But it also handles all the maintenance tasks required to provide this access. It creates and maintains a connection pool, handles authentication, SSL, and tracks the health of the deployment it is connected to, in the case of a Replica Set, this includes tracking which node is primary. Tracking the primary is important as it can change and the driver is able to route writes to the appropriate node. This class is designed to function as a Singleton as there can be a high cost to constructing it. This means we want to instantiate it as few times as possible. It would impact performance negatively to instantiate it on each request to our WebAPI.

So lets take a look at how to create a MongoClient and use it as a singleton in WebAPI. First, lets add the connection string to our configuration file. Add the MongoUri line to appsettings.Development.json:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "MongoUri": "mongodb://localhost:27017"
}

This is a MongoDB Connection String, currently just instructing the driver to connect to a mongod running on port 27017. At least that’s what it will do when we construct our MongoClient.

Next, in Startup.cs we will need to construct our MongoClient and pass it to our Dependency Injection provider. Inside ConfigureServices add the following line:

1
2
if (!string.IsNullOrWhiteSpace(Configuration["MongoUri"]))
    services.AddSingleton<MongoClient>(new MongoClient(Configuration["MongoUri"]));

This is a very basic way to get a MongoClient into the DI provider. This now allows us to take it as a constructor parameter in our controllers. Updating the default ValuesController to do this, we get

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IMongoClient _mongoClient;

    public ValuesController(MongoClient mongoClient)
    {
        _mongoClient = mongoClient;
    }

    /* The rest of the default class */
}

Now that we have a MongoClient in the ValuesController class, we can start performing CRUD operations with each API call. Lets start by accessing a database object. Then we need to get a collection object, which is where the data will actually be stored.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IMongoClient _mongoClient;
    private readonly IMongoDatabase _database;
    private readonly IMongoCollection<Value> _collection;

    public ValuesController(MongoClient mongoClient)
    {
        _mongoClient = mongoClient;
        _database = mongoClient.GetDatabase("api");
        _collection = _database.GetCollection<Value>("values");
    }

    /* The rest of the default class */
}

As you can see we now have a reference to a IMongoDatabase as well as an IMongoCollection<Value>. In order to get an IMongoDatabase, we need to provide the name of the database in MongoDB. If this is an existing database, the name must match what is shown in the Mongo Shell. An IMongoCollection<T> is accessed on the IMongoDatabase object and also requires a collection name. Notice the use of a type parameter. This type parameter is what tells the driver what types to expect from the collection. If an object is returned by the database which is not a Value, it will result in a serialization error. This will also prevent the insertion of data into the collection other than that of type Value.

The Value class, for this example is:

1
2
3
4
5
6
7
8
9
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;

public class Value
{
  [BsonRepresentation(BsonType.ObjectId)]
  public string Id { get; set; }
  public string Val { get; set; }
}

Note the use of ObjectId, which is MongoDB’s default _id type. The [BsonRepresentation(BsonType.ObjectId)] lets us use the string type at runtime in C# but persist it to MongoDB as an ObjectId. MongoDB does not have the concept of a monotonically increasing primary key, instead it uses the ObjectId type. This means our default controller methods will require a slight signature change to support querying by ObjectId as a string instead of int.

Lets fill in the rest of the default methods with code to access the values collection. Then we will look at a possible cleaner pattern for access.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Driver;
using MongoDB.Bson;

namespace webApiSample.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private readonly IMongoClient _mongoClient;
        private readonly IMongoDatabase _database;
        private readonly IMongoCollection<Value> _collection;

        public ValuesController(MongoClient mongoClient)
        {
            _mongoClient = mongoClient;
            _database = mongoClient.GetDatabase("api");
            _collection = _database.GetCollection<Value>("values");
        }

        // GET api/values
        [HttpGet]
        public async Task<ActionResult<IEnumerable<string>>> Get()
        {
            var values = await _collection.Find(Builders<Value>.Filter.Empty).ToListAsync();
            return new OkObjectResult(values);
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public async Task<ActionResult<string>> Get(string id)
        {
            var value = await _collection.Find(Builders<Value>.Filter.Eq(x => x.Id, id)).FirstOrDefaultAsync();
            if (value == null) return new NotFoundResult();
            return new OkObjectResult(value);
        }

        // POST api/values
        [HttpPost]
        public async Task Post([FromBody] string value)
        {
            await _collection.InsertOneAsync(new Value(){Val = value});
        }

        // PUT api/values/5
        [HttpPut("{id}")]
        public async Task Put(string id, [FromBody] string value)
        {
            await _collection.InsertOneAsync(new Value(){Id = id, Val = value});
        }

        // DELETE api/values/5
        [HttpDelete("{id}")]
        public async Task Delete(string id)
        {
            await _collection.DeleteOneAsync(Builders<Value>.Filter.Eq(x => x.Id, id));
        }
    }
}

Now we are ready to run our example. I will use Postman to interact with the API. Making a GET request to https://localhost:5001/api/values we receive

1
2
[]

Which is to be expected, as we have not added anything to our collection yet. So lets do that now. In Postman, configure a POST request to https://localhost:5001/api/values, on the Body tab, select raw, the type from Text to JSON (application/json) and type "foo" into the body text box. Press Send. If all went well, you should see an empty response of 200. Now lets make the first request again:

1
2
3
4
5
6
7
[
    {
        "id": "5b801da1fc8ba2080c540d86",
        "val": "foo"
    }
]

Now we can try making a DELETE request to remove this object. Using Postman, set the request type to DELETE and use the URL https://localhost:5001/api/values/5b801da1fc8ba2080c540d86. If we make a GET request on https://localhost:5001/api/values, we will again receive an empty array back.

So now that we have a basic WebAPI working with data from MongoDB, lets look at a cleaner way of granting access to MongoDB in our controllers. First, lets look at the work that our current ValuesController constructor is doing. It takes a MongoClient and uses this to (ultimately) get a reference to an IMongoCollection<Value> object. So why pass in the MongoClient at all? For that matter, why pass in an IMongoDatabase, the controller doesn’t (and shouldn’t) care about the underlying data store. With this in mind, we have a few options, depending mostly on personal preference.

  • Follow a repository model, wrapping all references to MongoDB in a concrete implementation of an IValuesRepository interface
  • Register IMongoCollection<Value> with our DI provider
  • Register some other type with our DI that itself has a reference to the IMongoCollection<Value>
  • Some combination of these options

I generally prefer to combine the repository model with another containing type. This is for a few reasons:

  • The repository model makes testing easier by allowing me to provide some sort of in-memory data store (great for testing)
  • The repository model makes my code agnostic to the persistence layer
  • A containing type makes it easy to, in one place, configure a MongoClient and do maintenance tasks (like creating indexes, controlling serializers, etc)

So lets look at what a containing type might look like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
using MongoDB.Driver;

public class MyMongoDatabase
{
    private readonly IMongoClient _mongoClient;
    private readonly IMongoDatabase _database;
    public MyMongoDatabase(string connectionString)
    {
        _mongoClient = new MongoClient(connectionString);
        _database = _mongoClient.GetDatabase("api");
        Values = _database.GetCollection<Value>("values");
    }

    public IMongoCollection<Value> Values { get; set; }
}

If we wanted to just stick with this type, we can register it with our DI provider and take it as a constructor parameter in ValuesController.

Updating our ConfigureServices method for the new type:

1
2
if (!string.IsNullOrWhiteSpace(Configuration["MongoUri"]))
  services.AddSingleton<MyMongoDatabase>(new MyMongoDatabase(Configuration["MongoUri"]));

Updating the ValuesController code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private readonly MyMongoDatabase _database;
public ValuesController(MyMongoDatabase database)
{
    _database = database;
}

// GET api/values
[HttpGet]
public async Task<ActionResult<IEnumerable<string>>> Get()
{
    var values = await _database.Values.Find(Builders<Value>.Filter.Empty).ToListAsync();
    return new OkObjectResult(values);
}

// GET api/values/5
[HttpGet("{id}")]
public async Task<ActionResult<string>> Get(string id)
{
    var value = await _database.Values.Find(Builders<Value>.Filter.Eq(x => x.Id, id)).FirstOrDefaultAsync();
    if (value == null) return new NotFoundResult();
    return new OkObjectResult(value);
}

// POST api/values
[HttpPost]
public async Task Post([FromBody] string value)
{
    await _database.Values.InsertOneAsync(new Value(){Val = value});
}

// PUT api/values/5
[HttpPut("{id}")]
public async Task Put(string id, [FromBody] string value)
{
    await _database.Values.InsertOneAsync(new Value(){Id = id, Val = value});
}

// DELETE api/values/5
[HttpDelete("{id}")]
public async Task Delete(string id)
{
    await _database.Values.DeleteOneAsync(Builders<Value>.Filter.Eq(x => x.Id, id));
}

Now it is very clear what data we are accessing (Values) and no matter what controller we inject the MyMongoDatabase object into, it will always be the same Values collection. Changing collection names, type, etc., now happens in one place in the application code. There is no longer a need to be concerned about passing the correct database or collection names to controllers or possibly setting the collection type incorrectly.

Taking this a step further, to use the repository pattern, we first need to create the correct repository interface.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
using System.Collections.Generic;
using System.Threading.Tasks;
using MongoDB.Bson;

public interface IValuesRepository
{
    Task<IEnumerable<Value>> GetAsync();
    Task<Value> GetAsync(string id);
    Task InsertAsync(Value val);
    Task DeleteAsync(string id);
}

Then we need to implement this repository around the MyMongoDatabase type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using System.Collections.Generic;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;

public class MongoValuesRepository : IValuesRepository
{
    private readonly IMongoCollection<Value> _values;
    public MongoValuesRepository(MyMongoDatabase database)
    {
        _values = database.Values;
    }
    public Task DeleteAsync(string id)
    {
        return  _values.DeleteOneAsync(Builders<Value>.Filter.Eq(x => x.Id, id));
    }

    public async Task<IEnumerable<Value>> GetAsync()
    {
        return await _values.Find(Builders<Value>.Filter.Empty).ToListAsync();
    }

    public Task<Value> GetAsync(string id)
    {
        return _values.Find(Builders<Value>.Filter.Eq(x => x.Id, id)).FirstOrDefaultAsync();
    }

    public Task InsertAsync(Value val)
    {
        return _values.InsertOneAsync(val);
    }
}

Now lets update the ConfigureServices method for the new type:

1
2
3
4
5
if (!string.IsNullOrWhiteSpace(Configuration["MongoUri"]))
{
    services.AddSingleton<MyMongoDatabase>(new MyMongoDatabase(Configuration["MongoUri"]));
    services.AddSingleton<IValuesRepository, MongoValuesRepository>();
}

Finally, updating the ValuesController:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private readonly IValuesRepository _values;
public ValuesController(IValuesRepository values)
{
    _values = values;
}

// GET api/values
[HttpGet]
public async Task<ActionResult<IEnumerable<string>>> Get()
{
    var values = await _values.GetAsync();
    return new OkObjectResult(values);
}

// GET api/values/5
[HttpGet("{id}")]
public async Task<ActionResult<string>> Get(string id)
{
    var value = await _values.GetAsync(id);
    if (value == null) return new NotFoundResult();
    return new OkObjectResult(value);
}

// POST api/values
[HttpPost]
public async Task Post([FromBody] string value)
{
    await _values.InsertAsync(new Value(){Val = value});
}

// PUT api/values/5
[HttpPut("{id}")]
public async Task Put(string id, [FromBody] string value)
{
    await _values.InsertAsync(new Value(){Id = id, Val = value});
}

// DELETE api/values/5
[HttpDelete("{id}")]
public async Task Delete(string id)
{
    await _values.DeleteAsync(id);
}

So now we have abstracted MongoDB away from our ValuesController, giving tight control over what operations are allowed within the ValuesController class while also making it (almost) completely agnostic to the persistence layer of the data.

This type of abstraction can be especially useful in large projects. Creating a Repositories project allows for multiple other projects to get access to the data layer in a controlled fashion. When there are large teams of developers with common access patterns, putting the CRUD logic inside an external library, makes updating queries much easier without risk of missing one. But it can also lead to a bloated number of methods in a particular repository if the access patterns aren’t shared across users of the library. By constraining the functionality downstream users have access to, operations can be constrained to ones with indexes or perhaps only delete operations that have an _id specified. Preventing accidental data loss.

As always, I hope this helps and happy programming.

comments powered by Disqus