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 package is the currently recommended package to install to use the MongoDB Driver. The other 3 (
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
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
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
Startup.cs we will need to construct our
MongoClient and pass it to our Dependency Injection provider. Inside
ConfigureServices add the following line:
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
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.
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 class, for this example is:
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
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.
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
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
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:
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
IMongoCollection<Value>with our DI provider
- Register some other type with our DI that itself has a reference to the
- 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
MongoClientand do maintenance tasks (like creating indexes, controlling serializers, etc)
So lets look at what a containing type might look like:
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
ConfigureServices method for the new type:
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.
Then we need to implement this repository around the
Now lets update the
ConfigureServices method for the new type:
Finally, updating the
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.