Introduction to MongoDb with .NET part 14: object serialisation in the .NET driver
April 21, 2016 4 Comments
Introduction
In the previous post we started working with the .NET driver for MongoDb. We started building a simple context class that acts as a handle on our MongoDb databases and collections. It will be a gateway to the documents in the collections as well. We also tested how to connect to the MongoDb server in code.
In this post we’ll continue to explore the .NET driver. In particular we’ll see how we can represent MongoDb documents in C# code. We’ll continue working on the demo .NET project we started in the previous post.
MongoDb serialisation
This topic is about building C# objects that can be serialised into BSON documents and back. I.e. when we build a C# object we can determine how it is serialised into a MongoDb document. Also, the same serialisation rules specify how a BSON document is mapped to a C# object. Keeping it simple we can say that if a Person object has a Telephone property it can be represented by a “phone” property in the corresponding BSON document.
The C# driver will translate to and from BSON and C# objects automatically through data (de)serialisation. The default serialisation is very simple. Say we have the following POCO:
public class Customer { public string Name { get; set; } public string Address { get; set; } }
The default JSON representation of this POCO will be:
{ "Name" : "Elvis" , "Address" : "Graceland" }
The POCO will be saved in the BSON format but the above JSON helps visualise the binary source. If the POCO has some sequence of objects, e.g.:
public class Customer { public string Name { get; set; } public string Address { get; set; } IEnumerable<string> Telephones { get; set; } }
The it’s translated into a JSON array:
{ "Name" : "Elvis" , "Address" : "Neverland" , "Telephones" : ["123", "456"] }
Nested objects such as…
public class Customer { public string Name { get; set; } public string Address { get; set; } IEnumerable<string> Telephones { get; set; } public WebPage PublicPage { get; set; } } public class WebPage { public bool IsSsl { get; set; } public string Domain { get; set; } }
…are represented as nested documents like here:
{ "Name" : "Elvis" , "Address" : "Neverland" , "Telephones" : ["123", "456"] , "PublicPage" : { "IsSsl": true, "Domain" : "company.com" } }
You probably see the pattern here. The properties of a C# object are translated into their nearest JSON representation.
Changing the default serialisation
Default serialisation is not always what we need. Take e.g. the documents in the demo Restaurants collection. The properties like “borough” and “cuisine” should be more like Borough and Cuisine to follow the C# property naming convention. Also, it is possible that a document property will get an entirely different name. Perhaps we don’t like the “restaurant_id” property and would like to call it simply “Id” in our Restaurant C# object. There are many other considerations, such as differing data types, null values, missing properties. E.g. an integer may be saved as string in a document but we want to have it as an Int32 in C#.
Serialisation mapping rules are achieved through various MongoDb attributes in the C# code. Let’s get a taste of these attributes now and see how a restaurant document can be mapped into a C# object. Add a new class called RestaurantDb to the Repository folder:
using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using Newtonsoft.Json; using System.Collections.Generic; namespace MongoDbDotNet.Repository { [BsonIgnoreExtraElements] public class RestaurantDb { [BsonId] [BsonElement(elementName:"_id")] public ObjectId MongoDbId { get; set; } [BsonElement(elementName:"address")] public RestaurantAddressDb Address { get; set; } [BsonElement(elementName:"borough")] public string Borough { get; set; } [BsonElement(elementName: "cuisine")] public string Cuisine { get; set; } [BsonElement(elementName:"grades")] public IEnumerable<RestaurantGradeDb> Grades { get; set; } [BsonElement(elementName: "name")] public string Name { get; set; } [BsonElement(elementName:"restaurant_id")] [BsonRepresentation(BsonType.String)] public int Id { get; set; } public override string ToString() { return JsonConvert.SerializeObject(this, Formatting.Indented); } } }
The class won’t compile just yet, we’ll add the missing objects in a bit.
You’ll need to add a reference to the Json.NET library through NuGet to compile this code:
Let’s see what each attribute does:
- [BsonIgnoreExtraElements]: we tell the serializer to ignore elements that figure in a JSON document but is not mapped in the C# object. This is useful if you add a new property to the documents directly in the DB
- [BsonId]: we declare that this property is the ID
- [BsonElement(elementName:”_id”)]: the BsonElement attribute with a string input is the way to declare what a C# property is called in the corresponding JSON document. In other words this attribute enables us to have different property names in the JSON document and the corresponding POCO
- [BsonRepresentation(BsonType.String)]: with this attribute we declare that a C# property type is different from its BSON/JSON counterpart. The restaurant IDs in the restaurant collection are stored as strings. However, we want to convert them to integers instead. Be careful with this attribute and make sure you test it on a large document sample. If a single document has a restaurant_id with a different type then data deserialisation will fail and a format exception will be thrown.
We can now add the missing ingredients to the Repository folder. First off we have the RestaurantAddressDb class:
using MongoDB.Bson.Serialization.Attributes; namespace MongoDbDotNet.Repository { [BsonIgnoreExtraElements] public class RestaurantAddressDb { [BsonElement(elementName: "building")] public string BuildingNr { get; set; } [BsonElement(elementName: "coord")] public double[] Coordinates { get; set; } [BsonElement(elementName: "street")] public string Street { get; set; } [BsonElement(elementName:"zipcode")] [BsonRepresentation(MongoDB.Bson.BsonType.String)] public int ZipCode { get; set; } } }
Also we have a RestaurantGradeDb class:
using MongoDB.Bson.Serialization.Attributes; using System; namespace MongoDbDotNet.Repository { [BsonIgnoreExtraElements] public class RestaurantGradeDb { [BsonElement(elementName: "date")] public DateTime InsertedUtc { get; set; } [BsonElement(elementName: "grade")] public string Grade { get; set; } [BsonElement(elementName: "score")] public object Score { get; set; } } }
The Score property is an odd one in that it is of type object. The problem is that not all scores are stored as integers in the database. Some of them are of BSON type null. Here’s an example:
{ "_id" : ObjectId("56edc2ff03a1cd840734f966"), "address" : { "building" : "725", "coord" : [ -74.01381169999999, 40.6336821 ], "street" : "65 Street", "zipcode" : "11220" }, "borough" : "Brooklyn", "cuisine" : "Other", "grades" : [ { "date" : ISODate("2015-01-20T00:00:00Z"), "grade" : "Not Yet Graded", "score" : null } ], "name" : "Swedish Football Club", "restaurant_id" : "41278206" }
The null type cannot be converted into a C# integer so I declared score as an object instead.
The next step is to add the appropriate handle around the restaurants collection in our ModelContext class. Collections are represented by the generic IMongoCollection interface in C#. Here comes the updated ModelContext class with a Restaurants property that acts as a handle on the restaurants collection. There are some other minor additions as well like the null checking logic in the Create method:
using MongoDB.Driver; using MongoDbDotNet.Infrastructure; using System; namespace MongoDbDotNet.Repository { public class ModelContext { private IMongoClient Client { get; set; } private IMongoDatabase Database { get; set; } private IConfigurationRepository ConfigurationRepository { get; set; } private static ModelContext _modelContext; private ModelContext() { } public static ModelContext Create(IConfigurationRepository configurationRepository, IConnectionStringRepository connectionStringRepository) { if (configurationRepository == null) throw new ArgumentNullException("ConfigurationRepository"); if (connectionStringRepository == null) throw new ArgumentNullException("ConnectionStringRepository"); if (_modelContext == null) { _modelContext = new ModelContext(); string connectionString = connectionStringRepository.ReadConnectionString("MongoDb"); _modelContext.Client = new MongoClient(connectionString); _modelContext.Database = _modelContext.Client.GetDatabase(configurationRepository.GetConfigurationValue("DemoDatabaseName", "model")); _modelContext.ConfigurationRepository = configurationRepository; } return _modelContext; } public void TestConnection() { var dbsCursor = _modelContext.Client.ListDatabases(); var dbsList = dbsCursor.ToList(); foreach (var db in dbsList) { Console.WriteLine(db); } } public IMongoCollection<RestaurantDb> Restaurants { get { return Database.GetCollection<RestaurantDb>(ConfigurationRepository.GetConfigurationValue("RestaurantsCollectionName", "restaurants")); } } } }
Let’s try this in the Main method of Program.cs. The following code sample will show an example of querying. Don’t worry about it too much yet, we’ll look at querying in the .NET driver in more detail later on. We want to filter out all restaurants that are located in the borough called Brooklyn. The “restaurants” variable will contain all the search results whereas “restaurant” will only hold a single value. You’ll see the difference between the ToList and FirstOrDefault operations that do the same thing as in LINQ to collections:
using MongoDB.Driver; using MongoDbDotNet.Infrastructure; using MongoDbDotNet.Repository; using System; namespace MongoDbDotNet { class Program { static void Main(string[] args) { ModelContext modelContext = ModelContext.Create(new ConfigFileConfigurationRepository(), new AppConfigConnectionStringRepository()); var filter = Builders<RestaurantDb>.Filter.Eq(r => r.Borough, "Brooklyn"); var restaurants = modelContext.Restaurants.FindSync<RestaurantDb>(filter).ToList(); var restaurant = modelContext.Restaurants.FindSync<RestaurantDb>(filter).FirstOrDefault(); Console.WriteLine(restaurant); Console.ReadKey(); } } }
You can add some code that iterates through the restaurants collection if you want. Otherwise a single restaurant will be printed in the console similar to the following:
{ "MongoDbId": "56edc2ff03a1cd840734dba8", "Address": { "BuildingNr": "2780", "Coordinates": [ -73.982419999999991, 40.579505 ], "Street": "Stillwell Avenue", "ZipCode": 11224 }, "Borough": "Brooklyn", "Cuisine": "American ", "Grades": [ { "InsertedUtc": "2014-06-10T00:00:00Z", "Grade": "A", "Score": 5 }, { "InsertedUtc": "2013-06-05T00:00:00Z", "Grade": "A", "Score": 7 }, { "InsertedUtc": "2012-04-13T00:00:00Z", "Grade": "A", "Score": 12 }, { "InsertedUtc": "2011-10-12T00:00:00Z", "Grade": "A", "Score": 12 } ], "Name": "Riviera Caterer", "Id": 40356018 }
Great, we have successfully implemented some basic serialisation for our restaurant objects in .NET.
We’ll continue with more serialisation examples in the next post.
You can view all posts related to data storage on this blog here.
Hi Andras,
Please, using design patterns, how to not smudge the domain layer with drivers MongoDB?
Hi Marcos, you can read the series dedicated to DDD starting here: https://dotnetcodr.com/2015/08/06/domain-driven-design-with-web-api-revisited-part-1-introduction/
One of the extensions to the demo project takes up MongoDb as the concrete repository. You’ll see an option there.
//Andras
Hi, great article series thanks. I’ve been trying to use the GeoJson data types in MongoDb.. which are supposedly catered for in the .Net MongoDb.Driver driver – however I’m finding that the GeoJson data types in the driver yield invalid GeoJson (according to the spec) when serialised via a webapi rest service. Any suggestions?
Thanks.
Tim
Hi its very useful but i get some other issue relatedthis topic kindly help me
public class CourseDocument
{
public ObjectId _id { get; set; }
public string Name { get; set; }
public List StudentAccess { get; set; }
public string EcosystemId { get; set; }
public CourseDocument()
{
StudentAccess = new List();
}
}
public class StudentObject
{
public long UserID { get; set; }
public long CluKey { get; set; }
}
in api method i am calling
var query = await _db.CourseDocuments.FindAsync(w => w.EcosystemId == acc.Identifier);
after this i am getting error “An error occurred while deserializing the StudentAccess property of class BankerCoreApi.Models.Courses.CourseDocument: Cannot deserialize a ‘List’ from BsonType ‘Document’.”