This time, a short post on how to model inheritance, which (at least in a class-oriented programming language) is one of the foundations of object-oriented programming.
Let’s take an example with a person, who has a home address that can be either domestic or foreign. Consider this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
public class Person { public string FirstName { get; set; } public string LastName { get; set; } public Address HomeAddress { get; set; } } public abstract class Address { public abstract string FormatAddress(string separator); } public class DomesticAddress : Address { public string Street { get; set; } public string Number { get; set; } public string PostalCode { get; set; } public string City { get; set; } // and 17 other fields here, according to whatever is standard in your country public override string FormatAddress(string separator) { return string.Join(separator, new[] { Street + " " + Number, PostalCode + " " + City }); } } public class ForeignAddress : Adress { public string[] AddressLines { get; set; } public override string FormatAddress(string separator) { return string.Join(separator, AddressLines); } } |
Now, when I create a Person with a DomesticAddress and save it to my local Mongo, it looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 |
var people = mongo.GetCollection<Person>(); people.Insert(new Person { FirstName = "Mogens Heller", LastName = "Grabe", HomeAddress = new DomesticAddress { Street = "Torsmark", Number = "4", // etc } }); |
which is all fine and dandy – and in the db:
1 2 3 4 5 6 7 8 9 10 |
> db.Person.findOne(); { "FirstName": "Mogens Heller", "LastName": "Grabe", "HomeAddress": { "Street": "Torsmark", "Number": "4", // etc... } } |
which looks pretty good as well. BUT when I try to load the person again by doing this:
1 2 |
var people = mongo.GetCollection<Person>(); var me = people.FindOne(); |
I get BOOM!!: Norm.MongoException: Could not find the type to instantiate in the document, and Address is an interface or abstract type. Add a MongoDiscriminatedAttribute to the type or base type, or try to work with a concrete type next time.
Why of course! JSON (hence BSON) only specifies objects – even though we consider them to be logical instances of some class, they’ re actually not! – they’re just objects!
So, we need to help NoRM a little bit. Actually the exception message says it all: Add a MongoDiscriminatedAttribute to the abstract base class, like so:
1 2 3 4 5 |
[MongoDiscriminated] public abstract class Address { public abstract string FormatAddress(string separator); } |
That was easy. Now, if I do a db.People.drop(), followed by my people.Insert(...)-code from before, I get this in the db:
1 2 3 4 5 6 7 8 9 10 11 |
> db.Person.findOne(); { "FirstName": "Mogens Heller", "LastName": "Grabe", "HomeAddress": { "__type": "MongoTest.DomesticAddress, MongoTest", "Street": "Torsmark", "Number": "4", // etc... } } |
See the __type field that NoRM added to the object? As you can see, it contains the assembly-qualified name of the concrete type that resulted in that particular object, allowing NoRM to deserialize properly when loading from the db.
Now, this actually makes working with inheritance hierarchies and specialization pretty easy – just add [MongoDiscriminated] to a base class, resulting in concrete type information being saved along with objects of any derived type.
Only thing that would be better is if NoRM would issue a warning or an exception when saving something that could not be properly deserialized – this way, one would not easily get away with saving stuff that could not (easily) be retrieved again.