Interacting with MongoDB using C#

In my previous two posts, I covered how to set up a MongoDB development environment and then followed this up with how to get started with MongoDB queries.

While it’s great to have a development environment set up and know how to run queries from the command line, ultimately you’ll need to figure out how to interact with MongoDB from code in order to build your applications.

This post explains how to interact with MongoDB from a .NET application using C#.

Prerequisites

Before you begin, firstly, make sure you have a MongoDB server available to connect to. Check out my Setting up a MongoDB development environment post for help with setting up a local MongoDB server instance. Alternatively, you can create a free MongoDB Cluster in the cloud via MongoDB Atlas.

Secondly, it helps to have some knowledge of the fundamental MongoDB concepts such as collections and documents, as well as the basics of how the MongoDB Query API works. I explain all that you need to know in my Getting started with MongoDB queries post.

Thirdly, you’ll need a suitable IDE to develop your application with. Visual Studio or Visual Studio Code are popular choices for .NET development. I’m currently using Visual Studio 2022.

Project setup

For the purposes of this walkthrough, a simple Console App will be fine for trying out the code samples. However, if you want to use a different project type such as an ASP.NET Core Web App that will work fine too.

Creating the project

After starting up Visual Studio, select the ‘Create a new project’ button and then select the ‘Console App’ (Recommended) or the ‘Console App (.NET Framework)’ option.

After choosing a name and location for your project, you’ll need to choose a Framework. If you need to use the .NET Framework you must target .NET Framework Version 4.7.2 or greater in order for the latest MongoDB C# Driver to be compatible. Otherwise, a .NET Core 2.0 or greater project, or a .NET Standard 2.0 or greater library will work.

I’m using .NET 6.0, as it is the latest .NET version at the time of writing this post and has Long-term support.

New templates

As a side note, I love the new Console App project template created by Visual Studio 2022 for .NET 6 projects. It features a single Program.cs file with the following contents.

// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

Yes, that’s right, there’s no using statements, no namespace, no class, and no Main method!

The new template uses recent C# features to greatly reduce the amount of boilerplate code needed for a simple project. Check out the link within the comment in the above code for more information.

NuGet package

After creating your new project, you will need to install the MongoDB.Driver NuGet Package into it. Currently, this package has over 67 million downloads, so it’s pretty popular!

You can install the NuGet package either via the ‘Manage NuGet Packages…’ option on the context menu that appears when you right-click on your project in Visual Studio or by running the following command from the Package Manager Console.

Install-Package MongoDB.Driver

When you install MongoDB.Driver the following NuGet packages will also be installed as dependencies.

  • MongoDB.Driver.Core
  • MongoDB.Bson
  • MongoDB.Libmongocrypt

The only NuGet package out of the above three that we will need to reference types from is the MongoDB.Bson package. A using statement for the MongoDB.Bson namespace needs to be added in order to access BSON-specific types and attributes.

Getting connected

The MongoDB C# Driver provides an asynchronous API for interacting with MongoDB servers and databases.

The driver does a nice job of aligning with the interface of MongoDB Query API. Therefore, the methods that the driver exposes should feel familiar if you are already up to speed with MongoDB query syntax or if you have read my previous article.

Before we can make full use of the methods and properties exposed by the driver, we need to get connected to a MongoDB database.

Connecting…

If you are connecting to a local MongoDB server, you can simply create a new MongoClient instance without specifying any arguments in the constructor (assuming you have no authentication for local development purposes).

var client   = new MongoClient();
var database = client.GetDatabase("movies_app");

In the above code, the connection attempt is made when the MongoClient object instance is created.

A reference to the database (IMongoDatabase) is then obtained by passing the name of the required database to the GetDatabase method.

Note that it’s important to bear in mind that pretty much everything is case-sensitive in MongoDB. This includes database names, collection names, and document field names.

As an alternative to the default constructor, you can pass the connection string you want to use. For a local connection, this will look similar to the code below.

var client = new MongoClient("mongodb://localhost:27017");

For a MongoDB Atlas connection, it takes a more complex format, as follows.

var client = new MongoClient("mongodb+srv://<username>:<password>@<cluster-address>/database-name");

Note that it is also possible to secure a local MongoDB server and you can pass along other details in the connection string such as the database name and other custom options.

Administrative functions

At this stage, we have a MongoClient and an IMongoDatabase instance that we can work with.

Let’s have a  look at some of the administrative functions that we can perform before trying out some queries.

Server-level operations

MongoClient allows us to perform server-level operations, such as listing database names.

var databaseNames = await client
.ListDatabaseNames()
.ToListAsync(); foreach (string databaseName in databaseNames) {    Console.WriteLine(databaseName); }

Or dropping (deleting) databases…

await client.DropDatabaseAsync("make_sure_this_is_the_correct_database_name");

You must be very careful when running server-level operations on production systems, especially destructive methods such as DropDatabase or DropDatabaseAsync which can result in lost data if they are misused.

Note that the DropDatabaseAsync method will not raise an exception if you specify the name of a database that doesn’t currently exist.

Database-level operations

In addition to the server-level operations, we can also perform database-level operations.

For example, we can list collection names for a database.

var collectionNames = await database
.ListCollectionNames()
.ToListAsync(); foreach (string collectionName in collectionNames) {    Console.WriteLine(collectionName); }

Or we can create, rename, and drop collections…

await database.CreateCollectionAsync("temp_collection");
await database.RenameCollectionAsync("temp_collection", "temp_collection_renamed"); await database.DropollectionAsync("temp_collection_renamed");

Note that MongoDB will automatically create a collection the first time we insert a document into a collection with a specific name if that collection does not currently exist. Therefore it isn’t usually necessary to create a collection directly in code.

Executing queries

Now that we’ve covered some of the basics of interacting with MongoDB at the server and database level, let’s move on to look at how we can execute queries from our code.

Untyped collection queries

Below is the simplest example possible of how to find documents in a specific collection that match some criteria and display the results. This is achieved without creating any additional types in our code.

var collection = database.GetCollection<BsonDocument>("movies");
var documents = await collection
.Find(new BsonDocument("year"1979))
.ToListAsync(); foreach (var document in documents) {     Console.WriteLine(document["title"]); }

The above code obtains a reference to a collection called ‘movies’. The object reference will be a generic IMongoCollection with the documents typed as BsonDocument which is a class defined in the MongoDB.Bson namespace.

The database isn’t queried until we call the Find method, followed by ToListAsync to materialise the results. The Find method has several overloads. In this case, a BsonDocument instance is passed in with the name and value for the field we want to match on i.e. the ‘year’.

Lastly, in the foreach loop, the value of the ‘title’ field is output by specifying its name to the BsonDocument indexer for every matching document.

Typed collection queries

Using BsonDocument is useful as a quick way of getting started, but it isn’t very practical to use across your application regardless of whether you’re working on a small or large project.

What you need instead is typed documents. This is where you define C# classes containing properties for the fields that you want to store in MongoDB and then let the MongoDB C# Driver take care of serializing and deserializing to and from instances of these classes.

To represent a movie document in code, we can define a class as follows.

/// <summary>
/// Represents a Movie entity.
/// </summary>
[BsonIgnoreExtraElements]
public class Movie
{
    /// <summary>
    /// The unique ID of the movie.
    /// </summary>
    public ObjectId Id { getset; }
 
    /// <summary>
    /// The Title of the movie.
    /// </summary>
    [BsonElement("title")]
    public string Title { getset; }
 
    /// <summary>
    /// The Year the movie was released in.
    /// </summary>
    [BsonElement("year")]     public int Year { getset; } }

Note that the BsonIgnoreExtraElements attribute prevents an exception from being raised when we execute queries on the movies collection since not all possible properties have been specified in the code (for the sake of simplicity for this walkthrough). The BsonElement attribute allows the driver to match the fields in the movies collection correctly since they are camel-cased. The driver automatically takes care of translating the Id property to the ‘_id’ MongoDB field.

Given this new class definition, we can now get a reference to a typed collection.

var moviesCollection = database.GetCollection<Movie>("movies");

This allows us to find documents in a much cleaner and type-safe manner.

var movies = await moviesCollection
    .Find(m => m.Year == 1979)
    .SortBy(m => m.Title)
    .ToListAsync();
 
foreach (var movie in movies)
{
    Console.WriteLine(movie.Title);
}

Now that we have a typed collection to work with, we can pass an expression to the Find method. This means that we can reference specific properties at compile-time instead of relying on brittle strings.

In the above code, the SortBy method is chained to the Find method, ordering the results by the ‘Title’ field.

In the foreach loop, we can now reference specific document fields by accessing the properties of the object.

LINQ

The standard Find method provided by the MongoDB C# Driver works really well and provides a lot of flexibility.

In addition to this, the MongoDB C# Driver includes a LINQ provider that allows you to write LINQ queries like you would for .NET in-memory collections or when using Entity Framework or Entity Framework Core as your ORM.

To avail of this, all you need to do is call the AsQueryable method on the IMongoCollection instance to get access to the LINQ functionality.

var movies = moviesCollection
    .AsQueryable()
    .Where(m => m.Year == 1979)
    .OrderBy(m => m.Title);

If you prefer Query Syntax instead of Method Syntax, no problem.

var movies = from    m in moviesCollection.AsQueryable()
             where   m.Year == 1979
             orderby m.Title
             select  m;

Note that not every LINQ operation is supported, but the LINQ provider covers all of the essentials.

It’s great that the MongoDB team have put in the effort to support LINQ and they have now developed a new and improved LINQ provider known as LINQ3 which is in beta testing at the time of writing.

Other operations

I’ve only scratched the surface of what’s possible with the MongoDB C# Driver, but before wrapping up, I would like to briefly make you aware of some of the other query methods and driver features that are available.

Common methods

Here are some of the most commonly used collection methods that are available for querying and updating data.

  • CountDocumentsAsync
  • DeleteManyAsync
  • DistinctAsync
  • InsertManyAsync
  • UpdateManyAsync

And there are many more…

Here’s an example of how to insert a document using the InsertOneAsync method.

await moviesCollection.InsertOneAsync(new Movie
{
    Title = "The Unforgivable",
    Year  = 2021
});

In addition to the methods mentioned above, the Aggregate method provides an interface for working with the very powerful MongoDB Aggregation Pipeline. This allows you to build up highly complex queries and aggregate data from different collections.

Conventions

One last thing I’d like to mention is the concept of conventions.

var conventionPack = new ConventionPack
{
    new CamelCaseElementNameConvention(),
    new IgnoreExtraElementsConvention(true)
};
 
ConventionRegistry.Register("CustomConventions", conventionPack_ => true);

The above code creates a ConventionPack object instance and at the same time adds two conventions to it.

The CamelCaseElementNameConvention tells the driver that document fields should be camel-case. This means that there’s no need to add the BsonElement attribute to your C# properties when you want to store all of your MongoDB document fields in camel-case.

As the name suggests, the IgnoreExtraElementsConvention causes extra elements in documents to be ignored. This prevents exceptions from being thrown if there are fields in a document that you haven’t defined properties for in your C# class.

The static Register method on the ConventionRegistry class is used to register the conventions contained in the convention pack with the driver. It’s important to register the conventions before getting a reference to your first collection and ideally, conventions should be registered as soon as possible when your application starts up before you create your first MongoClient instance.

Summary

And there you have it, that’s how to interact with MongoDB using C#!

In this post, I dived straight into the prerequisites and steps for setting up a .NET project with the MongoDB C# Driver.

I then demonstrated how to connect to a MongoDB server and perform some server-level and database-level operations.

Following this, I explained the basics of executing queries from code, demonstrating both untyped and typed collection queries and how to find documents based on specific criteria. I also looked at how to use LINQ to achieve the same results.

Lastly, I touched on some of the other query methods and how to set up driver conventions.

As mentioned in the previous section, I’ve only scratched the surface of what is possible, but I trust that you have found this post to be a useful introduction and that it will provide you with the knowledge needed to explore further.


I hope you enjoyed this post! Comments are always welcome and I respond to all questions.

If you like my content and it helped you out, please check out the button below 🙂

Comments