MongoDB Strongly-Typed Collection Usage Example

Using MongoDB in .NET is easy. However, there are two ways to manipulate the documents in C# code: raw bson document or stronly-typed document. In this article, we will compare the difference of them by examples. Basically, strongly-typed collection is preferred unless you have strong reason to use weakly-typed document (different types in the same collection?).

BsonDocument CRUD

The MongoDB C# Driver Official Document provide examples in this style. I guess the reason is that MongoDB is schemaless and the driver would like to demonstrate how to access document without schema. Actually, noSQL doesn't means no SQL but stands for not only SQL. Creating a schema for a collection is still recommended because it's easier to access documents and use index.

Here is the demo's document definition:

public class DemoEntry
{
    public string Uuid { get; set; }
    public string Value { get; set; }

    public override string ToString()
    {
        return Uuid + ": " + Value;
    }
}

Get a weakly-typed collection:

IMongoCollection<BsonDocument> _bsonCollection = database.GetCollection<BsonDocument>(CollectionName);

CRUD example by weakly-typed collection:

var entry = new DemoEntry
{
    Uuid = uuid,
    Value = "1"
};

Console.WriteLine("-- BsonDocument CRUD ---");
var filter = Builders<BsonDocument>.Filter.Eq("Uuid", uuid);
// Create
Console.WriteLine("[Create] " + entry);
await _bsonCollection.ReplaceOneAsync(filter, entry.ToBsonDocument(), new ReplaceOptions { IsUpsert = true });

// Read
var doc = await _bsonCollection.Find(filter).FirstOrDefaultAsync();
var retrievedEntry = BsonSerializer.Deserialize<DemoEntry>(doc);
Console.WriteLine("[Read] " + retrievedEntry);

// Update
await _bsonCollection.UpdateOneAsync(filter, Builders<BsonDocument>.Update.Set("Value", "2"));
doc = await _bsonCollection.Find(filter).FirstOrDefaultAsync();
retrievedEntry = BsonSerializer.Deserialize<DemoEntry>(doc);
Console.WriteLine("[Update] " + retrievedEntry);

// Delete
var t = await _bsonCollection.DeleteOneAsync(filter);
Console.WriteLine("[Delete] deleted count: " + t.DeletedCount);
Note that we need to manually convert the class to bsondocument before write operations (entry.ToBsonDocument()), and manually deserialize bsondocument to class after read operations (BsonSerializer.Deserialize<DemoEntry>(doc)). Besides, we need to construct a filter by providing document field name string, which may lead to errors.

Strongly-typed CRUD

Get a strongly-typed collection:

IMongoCollection<DemoEntry> _stronglyTypedCollection = database.GetCollection<DemoEntry>(CollectionName);

CRUD example:

public async Task StronglyTypedCrudAsync(string uuid)
{
	var entry = new DemoEntry
	{
		Uuid = uuid,
		Value = "1"
	};

	Console.WriteLine("-- Strongly typed CRUD ---");
	// Create
	Console.WriteLine("[Create] " + entry);
	await _stronglyTypedCollection.ReplaceOneAsync(x => x.Uuid == uuid, entry, new ReplaceOptions {IsUpsert = true});

	// Read
	var retrievedEntry = await _stronglyTypedCollection.Find(x => x.Uuid == uuid).FirstOrDefaultAsync();
	Console.WriteLine("[Read] " + retrievedEntry);

	// Update
	await _stronglyTypedCollection.UpdateOneAsync(x => x.Uuid == uuid, Builders<DemoEntry>.Update.Set(x => x.Value, "2"));
	retrievedEntry = await _stronglyTypedCollection.Find(x => x.Uuid == uuid).FirstOrDefaultAsync();
	Console.WriteLine("[Update] " + retrievedEntry);

	// Delete
	var t = await _stronglyTypedCollection.DeleteOneAsync(x => x.Uuid == uuid);
	Console.WriteLine("[Delete] deleted count: " + t.DeletedCount);
}

Strongly-typed or not?

Stronly-typed collection is recommended. The advantages are apparent:

  • Error-prune: document field name is provided in compile time instead of runtime.
  • Compact and clean code: no convertion is needed as the driver automatically do it.
  • LINQ friendly: it's much more promising for aggregation queries.

IgnoreExtraElements Convention

I think most of the MongoDB freshman will encounter the following error:

Element '_id' does not match any field or property of class

What's the _id field?

In MongoDB, each document stored in a collection requires a unique _id field that acts as a primary key. If an inserted document omits the _id field, the MongoDB driver automatically generates an ObjectId for the _id field.

However, we don't want to define such field in the class definition as it's useless for human. We can bypass the issue by two ways.

[BsonIgnoreExtraElements] Annotation

Adding [BsonIgnoreExtraElements] annotation:

[BsonIgnoreExtraElements]
public class DemoEntry
{
    public string Uuid { get; set; }
    public string Value { get; set; }

    public override string ToString()
    {
        return Uuid + ": " + Value;
    }
}
The drawback is that if you define the class in a commonly used interface library, the library must include the MongoDB driver nuget package, which is really annoying as the storage driver should be transparent for other developers.

Set IgnoreExtraElementsConvention

Another way is to set IgnoreExtraElementsConvention before getting strongly-typed collection:

var ignoreExtraElementsConvention = new ConventionPack { new IgnoreExtraElementsConvention(true) };
ConventionRegistry.Register("IgnoreExtraElements", ignoreExtraElementsConvention, type => true);

For stronly-typed collection, get collection and register convention order really matters:

_beingCollection = database.GetCollection<BsonDocument>(AvatarFrameworkMongoConfig.BeingCollectionName);

var ignoreExtraElementsConvention = new ConventionPack { new IgnoreExtraElementsConvention(true) };
ConventionRegistry.Register("IgnoreExtraElements", ignoreExtraElementsConvention, type => true);
If you do it like above, the error happens again:

Element '_id' does not match any field or property of class

Correct order:

var ignoreExtraElementsConvention = new ConventionPack { new IgnoreExtraElementsConvention(true) };
ConventionRegistry.Register("IgnoreExtraElements", ignoreExtraElementsConvention, type => true);

_beingCollection = database.GetCollection<BsonDocument>(AvatarFrameworkMongoConfig.BeingCollectionName);

For weakly-typed collection, the order doesn't matter. I guess the reason is when you creating a strongly-typed collection the BsonClassMap is configured using current ConventionRegistry.

Full Code

using System;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Conventions;
using MongoDB.Driver;

namespace MongoDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            var demo = new StronglyTypedDemo();
            demo.BsonCrudAsync("abc").Wait();
            Console.WriteLine("-----------------------------------");
            demo.StronglyTypedCrudAsync("abc").Wait();

            Console.ReadLine();
        }
    }

    public class DemoEntry
    {
        public string Uuid { get; set; }
        public string Value { get; set; }

        public override string ToString()
        {
            return Uuid + ": " + Value;
        }
    }

    public class StronglyTypedDemo
    {
        private const string DatabaseName = "";
        private const string CollectionName = "Test";
        private const string ConnectionString = "";

        private readonly MongoClient MongoClient;
        private readonly IMongoCollection<BsonDocument> _bsonCollection;
        private readonly IMongoCollection<DemoEntry> _stronglyTypedCollection;

        public StronglyTypedDemo()
        {
            var clientSettings = MongoClientSettings.FromConnectionString(ConnectionString);
            clientSettings.ConnectTimeout = TimeSpan.FromSeconds(5);
            clientSettings.ServerSelectionTimeout = TimeSpan.FromSeconds(5);
            clientSettings.AllowInsecureTls = true;

            MongoClient = new MongoClient(clientSettings);
            var database = MongoClient.GetDatabase(DatabaseName);

            var ignoreExtraElementsConvention = new ConventionPack { new IgnoreExtraElementsConvention(true) };
            ConventionRegistry.Register("IgnoreExtraElements", ignoreExtraElementsConvention, type => true);

            _bsonCollection = database.GetCollection<BsonDocument>(CollectionName);
            _stronglyTypedCollection = database.GetCollection<DemoEntry>(CollectionName);
        }

        public async Task StronglyTypedCrudAsync(string uuid)
        {
            var entry = new DemoEntry
            {
                Uuid = uuid,
                Value = "1"
            };

            Console.WriteLine("-- Strongly typed CRUD ---");
            // Create
            Console.WriteLine("[Create] " + entry);
            await _stronglyTypedCollection.ReplaceOneAsync(x => x.Uuid == uuid, entry, new ReplaceOptions {IsUpsert = true});

            // Read
            var retrievedEntry = await _stronglyTypedCollection.Find(x => x.Uuid == uuid).FirstOrDefaultAsync();
            Console.WriteLine("[Read] " + retrievedEntry);

            // Update
            await _stronglyTypedCollection.UpdateOneAsync(x => x.Uuid == uuid, Builders<DemoEntry>.Update.Set(x => x.Value, "2"));
            retrievedEntry = await _stronglyTypedCollection.Find(x => x.Uuid == uuid).FirstOrDefaultAsync();
            Console.WriteLine("[Update] " + retrievedEntry);

            // Delete
            var t = await _stronglyTypedCollection.DeleteOneAsync(x => x.Uuid == uuid);
            Console.WriteLine("[Delete] deleted count: " + t.DeletedCount);
        }

        public async Task BsonCrudAsync(string uuid)
        {
            var entry = new DemoEntry
            {
                Uuid = uuid,
                Value = "1"
            };

            Console.WriteLine("-- BsonDocument CRUD ---");
            var filter = Builders<BsonDocument>.Filter.Eq("Uuid", uuid);
            // Create
            Console.WriteLine("[Create] " + entry);
            await _bsonCollection.ReplaceOneAsync(filter, entry.ToBsonDocument(), new ReplaceOptions { IsUpsert = true });

            // Read
            var doc = await _bsonCollection.Find(filter).FirstOrDefaultAsync();
            var retrievedEntry = BsonSerializer.Deserialize<DemoEntry>(doc);
            Console.WriteLine("[Read] " + retrievedEntry);

            // Update
            await _bsonCollection.UpdateOneAsync(filter, Builders<BsonDocument>.Update.Set("Value", "2"));
            doc = await _bsonCollection.Find(filter).FirstOrDefaultAsync();
            retrievedEntry = BsonSerializer.Deserialize<DemoEntry>(doc);
            Console.WriteLine("[Update] " + retrievedEntry);

            // Delete
            var t = await _bsonCollection.DeleteOneAsync(filter);
            Console.WriteLine("[Delete] deleted count: " + t.DeletedCount);
        }
    }
}

Result:

-- BsonDocument CRUD ---
[Create] abc: 1
[Read] abc: 1
[Update] abc: 2
[Delete] deleted count: 1
-----------------------------------
-- Strongly typed CRUD ---
[Create] abc: 1
[Read] abc: 1
[Update] abc: 2
[Delete] deleted count: 1