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
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