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
|
|
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
:
|
|
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:
|
|
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
.
The 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 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.
|
|
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 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:
|
|
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:
|
|
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:
|
|
Updating the ValuesController
code:
|
|
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 MyMongoDatabase
type.
|
|
Now lets update the ConfigureServices
method for the new type:
|
|
Finally, updating the ValuesController
:
|
|
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.