MongoDB with .NET – Part 1

var result = await collection.DeleteManyAsync(x => x.Date < DateTime.Today);

var deletedCound = result.DeletedCount;

var result = await collection.DeleteManyAsync(x => x.Date < DateTime.Today);

var updated = result.DeletedCount;

Introduction

MongoDB is one of the most popular NoSQL databases out there, and for good reasons. For once, it does not have some of the limitations of more recent ones – I’m thinking of DocumentDB and Azure Table Storage, I really don’t know about the others –, is easy to get started with, and yet supports advanced scenarios, such as clustering and replication. Of course, it is also free and has an open source C# API! Winking smile

In this new series of posts, I will talk about how to perform common MongoDB operations with .NET. This won’t be a full blown tutorial, just a couple of “recipes”, if you like. The first post – this one – will cover what we might call DML operations – inserting, modifying, deleting and querying data. I will use LINQ for all querying, since it is the natural way to do queries in the .NET world.

You will need the following packages from Nuget, they are the official .NET packages from MongoDB.

image

image

image

I know very well that there are others, but I decided to stick with the official ones.

Connecting to MongoDB

For all the following examples, I will assume that I have the following object:

MongoClient client = new MongoClient();

This will establish a connection to a MongoDB instance running on the local server. If you need to connect to another one, you need to pass a URL with the following format:

mongodb://[username:password@]host[:port]

Mind you, the URL can be more complex, because it can take additional hosts as fallbacks, but let’s keep it simple for now. The username, password and port fields are of course optional. In the very minimum, if you want to connect to another server, this is what you’ll need:

MongoClient client = new MongoClient("mongodb://somehost");

Databases and Collections

The basic level of organization in MongoDB is the database. You always work in the context of a database. It’s the database that has the collections, functions and users (maybe another post). Documents, on the other hand, are stored in collections. Since collections are schemaless, you can store whatever you want in a collection, even totally unrelated and different documents.

Documents

A document in MongoDB is just some JSON content, or, more accurately, BSON. It can be represented in .NET by either a POCO class or by the BsonDocument class. BsonDocument allows programmatic access to its members, something that may come in handy. We can convert to and from using this code:

var obj = new Data { A = "1", B = 2 };

 

//conversion to BSON

var bson = obj.ToBsonDocument();

 

//conversion to JSON

var json = new bson.ToJson();

 

//round-trip

obj = (Data) BsonDocument.Create(bson);

 

obj = (Data) BsonDocument.Parse(json);

Setting a field is equally easy, the only thing is, you lose the strong typing:

var doc = new BsonDocument();

 

//check if a field exists

if (doc.Contains("Property") == false)

{

    doc["Property"] = "Value";

}

 

//all keys

var keys = doc.Elements.Select(x => x.Name);

 

//all values

var values = doc.Values.Select(x => BsonTypeMapper.MapToDotNetValue(x));

Inserting Documents

The first operation that you are likely to encounter is inserts. Like I said, in MongoDB, you can either work with strongly typed .NET classes (aka, POCOs) or with BsonDocument instances. Here’s how to go, first, with a POCO class:

var doc = new MyDocument();

//set document properties

 

var db = client.GetDatabase("database");

var collection = db.GetCollection<MyDocument>("collection");

 

await collection.InsertOneAsync(doc);

If instead you have a BsonDocument, it’s the same:

var doc = new BsonDocument();

//set document properties

 

var db = client.GetDatabase("database");

var collection = db.GetCollection<BsonDocument>("collection");

 

await collection.InsertOneAsync(doc);

Finally, you don’t even need a pre-defined document class, you can build one on the fly with an anonymous type or a dictionary, and you convert it into a BsonDocument:

//anonymous type

var doc = new { Property = "value" }.ToBsonDocument();

 

//dictionary

var dict = new Dictionary<string, object> { { “Property”, “value” } };

var doc = new BsonDocument(dict);

Another aspect to consider is the identifier. MongoDB always adds an _id property to all documents, where it stores its identifier. You have the choice to either supply your own value, or let MongoDB choose one for you. If you defined a class for your documents, you can have one property to map the _id property. You can use basic types for the identifier, like strings and numbers, but if you want MongoDB to take the responsibility to generate the identifier for you, you need to use the MongoDB type ObjectId:

public class MyDocument

{

    public ObjectId Id { get; set; }

    //rest goes here

}

I won’t cover conventions and mappings here, but you can use any other name for the identifier property, as long as you decorate it with the BsonIdAttribute attribute:

public class MyDocument

{

    [BsonId]

    public string Name { get; set; }

}

If you do not explicitly set a value to the identifier property, then MongoDB will provide you one, but you can certainly supply your own. The ObjectId structure can parse a string or accept a couple of other formats, and if you don’t use ObjectId, you can use whatever value you like, for example, strings and integers. After a successful insertion, you can get the generated identifier by inspecting the _id property of the BsonDocument (yes, you will need to convert the document to insert into a BsonDocument in order to retrieve the identifier, and no, unlike an ORM, the POCO class won’t be updated with the generated id):

var doc = new BsonDocument();

//set document properties

 

var db = client.GetDatabase("database");

var collection = db.GetCollection<BsonDocument>("collection");

 

await collection.InsertOneAsync(doc);

 

var id = (ObjectId) doc["_id"];

This example was for inserting a single document, but you can insert several at the same time:

var docs = myDocuments.Select(doc => doc.ToBsonDocument()).ToArray();

 

await collection.InsertManyAsync(docs);

 

var ids = docs.Select(doc => (ObjectId) doc["_id"]);

Updating Documents

When it comes to modifying existing document, you have two options:

  • Update parts of it;
  • Replace it altogether.

Let’s first cover updating parts of a document – mind you, this could even be all of the document’s properties minus the id (you cannot change an existing document’s identifier). Here is how it goes, for updating a single document; we need to set a filter and the updates that we want to do:

var update = new UpdateDefinitionBuilder<Data>().Set(x => x.Date, DateTime.Today.AddDays(1));

var result = await collection.UpdateOneAsync(x => x.Id == id, update);

var success = result.ModifiedCount == 1;

See how I add modifications to a builder, in this example, I am just setting a new value for the Date property.

This is the strongly typed version, using POCOs, but you can also do with BsonDocument:

var update = new UpdateDefinitionBuilder<BsonDocument>().Set(x => x["Date"], DateTime.Today.AddDays(1));

var result = await collection.UpdateOneAsync(x => x["_id"] == id, update);

Like I previously shown, a dictionary of key-values can be converted to a BsonDocument, and the same goes to an anonymous type.

You can do several modifications at the same time. For that, you just add modification items to the UpdateDefinitionBuilder<T> object:

var update = new UpdateDefinitionBuilder<Data>()

    .Set(x => x.Date, DateTime.Today.AddDays(1))

    .Inc(x => x.Number, 1)

    .Unset(x => x.Name);

All of these will be translated to native MongoDB update expressions. At the moment, it is not possible to update fields based on other fields, just some basic operations:

  • Set/unset a value;
  • Set a value to be the maximum/minimum of a two values (the current field value and another one);
  • Increment a number by some value (can be negative);
  • Add/remove something from a collection;
  • etc.

As for changing several documents at once, easy easy:

var update = new UpdateDefinitionBuilder<BsonDocument>().Set(x => x["Date"], DateTime.Today.AddDays(1));

var result = await collection.UpdateManyAsync(x => x["Date"] == DateTime.Today, update);

Just make sure that you do not supply an _id property with a value other than the current one!

Replacing Documents

When you replace a document you lose all of the existing properties, minus, of course, the id. You don’t need to follow the previous schema, you can use totally different contents:

var result = await collection.ReplaceOneAsync(c => c.Id == id, new Data { Date = DateTime.Today.AddDays(1), SomeNewField = "Some Value" });

Again, the replacement document cannot have a value for the identifier property that is different from the one that we are trying to update. It is OK to not include one at all.

It is not possible to replace many documents in a single shot.

Querying Documents

For querying a strongly typed collection, we can use LINQ:

var doc = collection.AsQueryable().SingleOrDefault(x => x.Id == id);

Or, if we are working with BsonDocuments and we want to build queries dynamically:

var filter = Builders<BsonDocument>

    .Filter

    .And

    (

        Builders<BsonDocument>.Filter.Gt(x => x["Date"], DateTime.Today.AddDays(-7)),

        Builders<BsonDocument>.Filter.Lt(x => x["Date"], DateTime.Today.AddDays(7))

    );

 

var cursor = await collection.FindAsync(filter);

var docs = new List<BsonDocument>();

 

while (await cursor.MoveNextAsync() == true)

{

    docs.AddRange(cursor.Current);

}

The LINQ syntax supports paging:

var docs = collection.AsQueryable().OrderBy(doc => doc.Number).Skip(startIndex).Take(pageSize).ToList();

And, of course, you can also do it dynamically on a BsonDocument, with a little more work:

var sort = new SortDefinitionBuilder<BsonDocument>().Ascending(doc => doc["Number"]);

var cursor = await collection.FindAsync(FilterDefinition<BsonDocument>.Empty, new FindOptions<BsonDocument> { Limit = pageSize, Skip = startIndex, Sort = sort });

var docs = new List<BsonDocument>();

 

while (await cursor.MoveNextAsync() == true)

{

    docs.AddRange(cursor.Current);

}

Finally, here’s how to do projections using LINQ syntax:

var names = collection.AsQueryable().Select(x => x.Name).ToList();

And dynamically defined fields:

var options = new FindOptions<BsonDocument> { Projection = Builders<BsonDocument>.Projection.Include("Name") };

var cursor = await collection.FindAsync(FilterDefinition<BsonDocument>.Empty, options);

var names = new List<string>();

 

while (await cursor.MoveNextAsync() == true)

{

    names.AddRange(cursor.Current.Where(x => x.Contains("Name") == true).Select(x => BsonTypeMapper.MapToDotNetValue(x["Name"]) as string));

}

Deleting Documents

This one’s easy:

var result = await collection.DeleteManyAsync(x => x.Date < DateTime.Today);

You can also do it for a single document, the API is almost identical:

var result = await collection.DeleteOneAsync(x => x.Date < DateTime.Today);

MongoDB vs ORMs

MongoDB is not an ORM, which means that some things that you may be used to – lazy loading, change tracking, first level cache, Unit of Work and transactions, etc – do not apply. Having said that, it is totally possible to hide both an ORM and MongoDB under a common abstraction, like the Repository pattern. Maybe a topic for some other time!

Conclusion

Hope this was enough to raise your interest! The next post will be about DDL operations, in the meantime, I’d be happy to hear from you!

Entity Framework 7: What’s New?

The Microsoft WebCamp 2015 event (Portugal) is now over! Another day packed with lots of interesting sessions and, above all, interesting people! Smile

As usual, here is the slide deck for my presentation, Entity Framework 7: What’s New?. In the shared folder you will also find the sample code. *Please*, do drop me a line if you want to discuss any of the topics!

Thanks to all who dropped by, and to Luís Calado (@elcalado) and João Almeida (@jalmeida) from Microsoft Portugal for inviting me! Winking smile