The code that accompanies this article can be received after subscription

* indicates required

The world of NoSQL really boomed in the 10s. It gave us more options in choosing databases than in the past few decades. Now, we can think about the nature of your data and system architecture, and actually pick the type of database that best suits our needs.

While relational databases are structured and have predefined schemas, Non-relational databases are unstructured, distributed, and have dynamic schemas. One of the coolest things about the NoSQL ecosystem is that it contains many databases and each type is best applied to different kinds of problems.

ML.NET Full-Stack: Complete Guide to Machine Learning for .NET Developers

From the basics of machine learning to more complex topics like neural networks, object detection and NLP, this course will guide you into becoming ML.NET superhero.

One of the dominant databases in the NoSQL market at the moment is undoubtedly MongoDB. It is a document NoSQL database, and thus closer to the traditional relational databases than some other types of NoSQL databases. This can probably explain its success. In this article, we are gonna see how we can use MongoDB in C#.

Table of content:

1. MongoDB Basics

Before we see the different ways of using MongoDB with C#, let’s go through some of the basic concepts of this database. Guys from MongoDB are very proud of what they call Nexus Architecture. This architecture gives the ability to combine proven concepts and abilities of relational databases with NoSQL innovations.

1.1 MongoDB Documents

MongoDB is a document-oriented database, and as already mentioned, it has certain similarities to relational databases. Instead of rows, MongoDB has documents. All the data for a given record is stored in a single document, different from relational databases where information for a given record is spread across many tables. Under the hood, documents are BSON files, which are bin­ary-en­coded seri­al­iz­a­tion of JSON files. Nevertheless, from a programmer’s point of view, MongoDB manipulates with pure JSON files. For example, we can represent a user like this:

{
    "_id" : ObjectId("58e28d41b1ad7d0c5cd27549"),
    "name" : "Nikola Zivkovic",
    "blog" : "rubikscode.net",
    "numberOfArticles" : 10,
    "Adress" : [
        "street" : "some street",
        "city" : "Berlin",
        "country" : "Germany"
        ],
    "expertise" : [".NET", "Machine Learning", "Deep Learning", "Management", "Leadership"]
}

As you can notice there is an _id field at the beginning of this JSON document. This field is unique and is generated by MongoDB for every document in the database. This way MongoDB has kept one of the important properties of relational databases – strong consistency.

1.2 MongoDB Collections

Documents are stored inside collections. Collections are groups of somehow related documents, but these documents don’t need to have the same structure. Here lies one of the biggest benefits of MongoDB. Developers don’t need to know the schema of the database beforehand but can modify the schema dynamically during development.

Data Science Visual

This is especially great in the systems where we can’t get the schema quite right in the beginning, or there are plenty of edge cases to cover. Also, this way the entire problem with impedance mismatch is avoided, i.e. elimination of object-relational mapping layer is eliminated. What does this look like?

Well, let’s say that the previous document is stored in the collection called “users”. Then we could add another document in that collection, which will contain fields that the previous document doesn’t have and/or won’t have fields that the previous document has. For example, we could add the next document into the collection:

{
    "_id" : ObjectId("58e28da0b1ad7d0c5cd2754a"),
    "name" : "Vanja Zivkovic",
    "blog" : "abrahadabra.rs",
    "Adress" : [
        "street" : "some street",
        "city" : "Berlin",
        "country" : "Germany"
        ],
    "expertise" : ["Event Management", "SEO", "Marketing", "Social Media"]
    "location" : [45, 19]
}

These documents are similar, but not the same. Collection groups them, and gives you the ability to add indexes to these documents. Indexes are one of the concepts that MongoDB inherited from the relational databases in the same form.

1.3 MongoDB and Relational Databases Differences

It’s important to emphasize some of the other differences between MongoDB and Relational databases. Firstly, MongoDB doesn’t have foreign keys. But, it has a feature that looks quite like that – References. Basically, any object can have a reference to some other object, using its id, but this is not automatically updated, and it’s up to the application to keep track of these connections.

This is done this way due to the fact that once a foreign key is introduced in a relational database, it can be hard to unwind it from it. Thanks to the document data model, and due to the fact that all the necessary information for one “record” is stored inside one document, joins are not provided in MongoDB. However, a similar mechanism called Lookup is available. Among other differences, it should be mentioned that there are no multiple-table transactions in MongoDB.

In this section I haven’t covered CRUD opperations from the shell, replica sets and sharding. If you want to learn more about this you can do so here and here.

Data Visual

1.4 Different ways to deploy MongoDB

There are different ways to install and deploy this NoSQL Database. You could install it in your local computer or server, or you could use cloud option – MongoDB Atlas. In this article, we use the former. There are several available packages and in this tutorial we use community package. Here we create one cluster and deploy it in Azure.

MongoDB C# - MongoDB Atlas

For purposes of this exercise, we create database called – blog. This database has a collection – users, and this collection contains document for each user.

C# and MongoDB - Database and Collection

2. Installing MongoDB Driver

Guys from MongoDB provided a wide range of drivers for different programming languages. Drivers are just client libraries that applications can use to communicate with Mongo Database. For .NET all we need to do is install the NuGet package:

Install-Package MongoDB.Driver

Or if you are using dotnet CLI:

dotnet add package MongoDB.Driver

Note that in this implementation we use .NET 6 with Driver version 2.17.

3. Mapping BSON to strongly typed C# object

If you checked out my previous posts, you’ve learned that MongoDB stores documents in BSON format. Those are basically binary JSON files (ok, it is a little bit more complicated than that :)). When we use .NET driver we consume documents trough BsonDocumentWhat we want to do in this exercise is to map those BsonDocuments to strongly typed C# objects. But before that, let’s first see how our database and its entities look like.

As already mentione, we use database called – blog which has a collection – users. JSON document for the individual user would look like something like this:

{ 
  "_id" : ObjectId("59ce6b34f48f171624840b05"), 
  "name" : "Nikola", 
  "blog" : "rubikscode.net", 
  "age" : 34, 
  "location" : "Berlin" 
}

An equivalent of this in C# looks like this:

using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;

namespace MongoDb
{
    /// <summary>
    /// Class used in business logic to represent user.
    /// </summary>
    public class User
    {
        [BsonId]
        public ObjectId Id { get; set; }
        [BsonElement("name")]
        public string Name { get; set; }
        [BsonElement("blog")]
        public string Blog { get; set; }
        [BsonElement("age")]
        public int Age { get; set; }
        [BsonElement("location")]
        public string Location { get; set; }
    }
}

You will notice that there are attributes for each property of this class. These attributes allow us to automatically map data from BsonDocument to our class. BsonId is used to map unique document identifier. Every document in MongoDB has this element, and its type is ObjectIdBsonElement(field_name) is used to map other fields from the object on the object properties.

Programming Visual

Since we know that document databases don’t have a strict schema, it is possible to have more fields in the JSON document then we really want to use in our application. Also, it is possible that we don’t want to pick up all the fields from the database. For this, we can use BsonIgnoreExtraElements attribute on the class itself.

4. C# and MongoDB Repository Implementation

The main goal of this exercise is to create a class which will give us the ability to do simple CRUD operations on users collection. The first thing we need to do is connect to the database from our application. The easiest way to do this is to use MongoClient class. It accepts connection string so we will have to provide this.

Alternatively, MongoClientSettings class can be used, which provide various possibilities. One of the interesting ones is ClusterConfiguration property, which is of ClusterBuilder type, used for configuring clusters. Other classes that we need to use are MongoDatabaseto access defined database (blog in this case), and MongoCollectionto access defined collection (users in this case). Here is how that looks like in the code:

/// <summary>
/// Class used to access Mongo DB.
/// </summary>
public class UsersRepository
{
    private IMongoClient _client;
    private IMongoDatabase _database;
    private IMongoCollection<User> _usersCollection;

    public UsersRepository(string connectionString)
    {
        _client = new MongoClient(connectionString);
        _database = _client.GetDatabase("blog");
        _usersCollection = _database.GetCollection<User>("users");
    }
}
Coding Visual

Ok, this is pretty straightforward. Still, there is one thing that should be noted. MongoCollection is defined using Users as a template type, and this is possible only because we added those attributes to Users class. The other way to achieve this is by using BsonDocument, but then we have to map fields manually to properties of Users class.

3.1 Create Operation with C# and MongoDB

Inserting document in the database is done easily once previous steps have been followed:

public async Task InsertUser(User user)
{
    await _usersCollection.InsertOneAsync(user);
}

Obviously, I have used asynchronous operation InsertOneAsync, but you can use synchronous one too. Again, because we mapped JSON fields on Users properties, it is easy to work with these operations.

3.2 Read Operation with C# and MongoDB

Reading users is done like this:

public async Task<List<User>> GetAllUsers()
{
    return await _usersCollection.Find(new BsonDocument()).ToListAsync();
}

public async Task<List<User>> GetUsersByField(string fieldName, string fieldValue)
{
    var filter = Builders<User>.Filter.Eq(fieldName, fieldValue);
    var result = await _usersCollection.Find(filter).ToListAsync();

    return result;
}

public async Task<List<User>> GetUsers(int startingFrom, int count)
{
    var result = await _usersCollection.Find(new BsonDocument())
                                       .Skip(startingFrom)
                                       .Limit(count)
                                       .ToListAsync();

    return result;
}

There are three different implementations of this functionality. Let’s go through each of them.

GetAllUsers returns all users from the database. We use the Find method of the MongoCollection class to do so, and pass empty BsonDocument into it. In the next method, GetUsersByField method, we can see that this Find method actually receives filter object, which it will use as criteria for getting the data.

In the first function, we use an empty filter and thus receive all users from the collection. In second, we use Builder to create the filter which will be used against the database. Finally, the last function – GetUsers uses Skip and Limit methods of MongoCollection to get a necessary chunk of data. For example, this last function can be used for paging.

Programming Visual

3.3 Update Operation with C# and MongoDB

Documents can be updated in a similar manner:

public async Task<bool> UpdateUser(ObjectId id, string udateFieldName, string updateFieldValue)
{
    var filter = Builders<User>.Filter.Eq("_id", id);
    var update = Builders<User>.Update.Set(udateFieldName, updateFieldValue);

    var result = await _usersCollection.UpdateOneAsync(filter, update);

    return result.ModifiedCount != 0;
}

In this example, function UpdateOneAsync is used, but if you want to change values of multiple documents you can use UpdateMultipleAsync. Also, synchronous siblings of these operations also exist.

Another interesting fact is that using this function you can add fields that are not in the “schema”. For example, if you want to add a new field in the user document, you can do this:

var users = await _mongoDbRepo.GetUsersByField("name", "Nikola");
var user = users.FirstOrDefault();

var result = await _mongoDbRepo.UpdateUser(user.Id, "address", "test address");

This way the big MongoDB’s (and rest of the document databases) feature of a modular schema is harnessed. You are not blocked by defined objects, you can store information dynamically if you need to do so.

3.4 Delete Operation with C# and MongoDB

And finally, users can be deleted this way:

public async Task<bool> DeleteUserById(ObjectId id)
{
    var filter = Builders<User>.Filter.Eq("_id", id);
    var result = await _usersCollection.DeleteOneAsync(filter);
    return result.DeletedCount != 0;
}

public async Task<long> DeleteAllUsers()
{
    var filter = new BsonDocument();
    var result = await _usersCollection.DeleteManyAsync(filter);
    return result.DeletedCount;
}

Pretty straightforward, don’t you think? Same as in Reading examples, Builder was used for creating a filter.

3.5 Creating Indexes with C# and MongoDB

Adding indexes is a bit different. If we want to do it dynamically, the easiest way is to use BsonDocument(. Here is how it is done:

public async Task CreateIndexOnCollection(IMongoCollection<BsonDocument> collection, string field)
{
    var keys = Builders<BsonDocument>.IndexKeys.Ascending(field);
    await collection.Indexes.CreateOneAsync(keys);
}

On the other hand, if we know what our indexes will be beforehand, we can use strongly typed implementation like this:

public async Task CreateIndexOnNameField()
{
    var keys = Builders<User>.IndexKeys.Ascending(x => x.Name);
    await _usersCollection.Indexes.CreateOneAsync(keys);
}

Conclusion

Here, in one simple example, we went through many of MongoDB Driver’s features. We learned how to map JSON documents to .NET objects, which gave us an easy way to use rest of the features. CRUD operations, which are most commonly used, have been showed and explained, too.

In general, one could see how to use MongoDB features with C# in .NET environment.

ML.NET Full-Stack: Complete Guide to Machine Learning for .NET Developers

From the basics of machine learning to more complex topics like neural networks, object detection and NLP, this course will guide you into becoming ML.NET superhero.

Discover more from Rubix Code

Subscribe now to keep reading and get access to the full archive.

Continue reading