MongoDB强类型Collection用法示例
.NET中使用MongoDB非常简单,一般来说可以直接使用BsonDocument,也可以使用定义好的数据类型对文档进行CRUD操作。本文通过实例对比一下两种方式的优劣,通常,通过强类型Collection对文档进行操作更为便捷。
BsonDocument CRUD
MongoDB
C# Driver官方文档
是直接用BsonDocument进行操作的,猜想官方文档可能是想突出体现MongoDB是schema-less的,可以不定义schema而读写文档。但noSQL不代表schema-less,而是not only SQL。在同一个collection中还是推荐使用统一的data
model,因为统一的schema可以更方便读写,而且也方便使用索引。
下文将使用的示例文档定义:
public class DemoEntry
{
    public string Uuid { get; set; }
    public string Value { get; set; }
    public override string ToString()
    {
        return Uuid + ": " + Value;
    }
}获取一个弱类型collection:
IMongoCollection<BsonDocument> _bsonCollection = database.GetCollection<BsonDocument>(CollectionName);使用弱类型collection进行CRUD:
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);entry.ToBsonDocument()),同时在读操作之后手动将bsondocument解析成class
(BsonSerializer.Deserialize<DemoEntry>(doc))。此外,我们在构建filter时需要手动输入field名称的字符串,很容易出错,而且是运行时错误。
强类型CRUD示例
获取一个强类型collection:
IMongoCollection<DemoEntry> _stronglyTypedCollection = database.GetCollection<DemoEntry>(CollectionName);CRUD示例:
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);
}是否要使用强类型collection?
推荐使用强类型collection。 优点非常明显:
- 不易出错:使用lambda表达式指定filter,在编译时就确定了field name
- 代码简捷:不需要手动在class和bsondocument之间转换。
- LINQ语法支持:这一点在使用aggregation的时候极为便捷,一行代码就能处理十行的一个aggregation pipeline。
IgnoreExtraElements Convention
大部分MongoDB新用户想必都会遇到如下错误:
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.
但在代码中我们往往不想显式定义这么个字段,因为它对开发者来说没有太多意义。可以通过两种方法解决这个问题。
[BsonIgnoreExtraElements] Annotation
在类定义中加上[BsonIgnoreExtraElements] annotation:
[BsonIgnoreExtraElements]
public class DemoEntry
{
    public string Uuid { get; set; }
    public string Value { get; set; }
    public override string ToString()
    {
        return Uuid + ": " + Value;
    }
}设置IgnoreExtraElementsConvention
另一种方案是在获取collection之前设置IgnoreExtraElementsConvention:
var ignoreExtraElementsConvention = new ConventionPack { new IgnoreExtraElementsConvention(true) };
ConventionRegistry.Register("IgnoreExtraElements", ignoreExtraElementsConvention, type => true);IgnoreExtraElementsConvention的意思是在反序列化文档时,忽略映射类中未定义的字段。
对强类型collection的用法,设置IgnoreExtraElementsConvention和获取collection的顺序非常重要:
_beingCollection = database.GetCollection<BsonDocument>(AvatarFrameworkMongoConfig.BeingCollectionName);
var ignoreExtraElementsConvention = new ConventionPack { new IgnoreExtraElementsConvention(true) };
ConventionRegistry.Register("IgnoreExtraElements", ignoreExtraElementsConvention, type => true);Element '_id' does not match any field or property of class
正确设置方法:
var ignoreExtraElementsConvention = new ConventionPack { new IgnoreExtraElementsConvention(true) };
ConventionRegistry.Register("IgnoreExtraElements", ignoreExtraElementsConvention, type => true);
_beingCollection = database.GetCollection<BsonDocument>(AvatarFrameworkMongoConfig.BeingCollectionName);ConventionRegistry设置BsonClassMap。
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);
        }
    }
}运行结果:
-- 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