C# - Default discriminator convention

889 views
Skip to first unread message

Gabriel Duford

unread,
Mar 28, 2011, 1:17:55 PM3/28/11
to mongod...@googlegroups.com
Hi,
I am evaluating the work required to migrate from the non-official driver (samus) to the official one. I found out that the default discriminator for inheritance is the Hierarchical one, and this is kind of "hard-coded" in sonDefaultSerializer.LookupDiscriminatorConvention. Unless I am missing something, it seems that to override this, a discriminator must be set for every base type participating in serialization.
Is there a way to set the default discriminator convention to use when one is not explicitly set on a type?
If I load an entity that was persisted with the non-official driver (which uses the equivalent of the ScalarDiscriminatorConvention), will it load properly with the official driver?
Thanks,

gabriel

Robert Stam

unread,
Mar 28, 2011, 2:20:36 PM3/28/11
to mongodb-user
Suppose we have the following classes:

public class A {
public int a;
}

public class B : A {
public int b;
}

public class C : B {
public int c;
}

And we serialize an instance of C as follows:

var c = new C { a = 1, b = 2, c = 3 };

Console.WriteLine(c.ToJson<A>()); // nominalType is A
Console.WriteLine(c.ToJson<B>()); // nominalType is B
Console.WriteLine(c.ToJson<C>()); // nominalType is C

we get:

{ "_t" : "C", "a" : 1, "b" : 2, "c" : 3 }
{ "_t" : "C", "a" : 1, "b" : 2, "c" : 3 }
{ "a" : 1, "b" : 2, "c" : 3 }

So there are a couple of things to note:

1. A discriminator is only written if it is "needed"
2. The default discriminator is actually a scalar discriminator

One definition of a discriminator being "needed" is that the actual
type is different from the nominal type, which is true in the first 2
out of these 3 examples.

A hierarchical discriminator is only used when there is a "RootClass".
For example, we can make A a root class like this:

[BsonDiscriminator(RootClass = true)]
public class A {
public int a;
}

Now if we run the same three JSON output statements we get:

{ "_t" : ["A", "B", "C"], "a" : 1, "b" : 2, "c" : 3 }
{ "_t" : ["A", "B", "C"], "a" : 1, "b" : 2, "c" : 3 }
{ "_t" : ["A", "B", "C"], "a" : 1, "b" : 2, "c" : 3 }

And we see that the discriminators are arrays instead of scalars
because the presence of the RootClass attribute signals to use a
hierarchical discriminator.

During deserialization it actually doesn't matter whether the
discriminator is a scalar or an array. It if is an array only the last
element is used.

Given all this you may not have to set the default discriminator
convention. You probably have all the control you want just by using
or not using RootClass.

A document stored in a database using the samus driver can be loaded
successfully into an equivalent class using the official C# driver
assuming you use the same class name and property names. Or if they
differ in any way then you have to configure the class maps
appropriately.

I'll be happy to help further if you need.

Good luck!

Robert

Gabriel Duford

unread,
Mar 28, 2011, 2:54:16 PM3/28/11
to mongod...@googlegroups.com, Robert Stam
Thanks Robert.
I indeed set the the base class as the root because I wanted the "_t" entry to be serialized.
If we reuse your class hierarchy, the case I want to support is to have a collection of A instances in Mongo and be able to reload the instances from it with their nominal type. If I save the above 'c' instance in a MongoCollection<A>, although it saves all the properties of A, B and C, I cannot deal with a generic collection of A containing instances of A, B and C after loading.
Maybe there's something I don't understand?
Thanks again,

gabriel

Robert Stam

unread,
Mar 28, 2011, 3:11:27 PM3/28/11
to mongodb-user
Not having seen any of your code I'm not sure where you are running
into trouble, but my guess is that when you Insert or Save your
document you are not specifying the nominal type (and therefore no
discriminator is being written). Here's a short sample program that
writes an A, a B and a C to a collection and reads it back (using
discriminators and polymorphism):

http://www.pastie.org/1727620

Here's the output from the mongo shell showing what actually got saved
to the collection:

> db.test.find()
{ "_id" : ObjectId("4d90dadbe447ad2a10c2ba72"), "a" : 1 }
{ "_id" : ObjectId("4d90dadbe447ad2a10c2ba73"), "_t" : "B", "a" : 1,
"b" : 2 }
{ "_id" : ObjectId("4d90dadbe447ad2a10c2ba74"), "_t" : "C", "a" : 1,
"b" : 2, "c" : 3 }
>

Notes:

1. I had to add an Id field to class A now that I'm saving things to
the database
2. The collection variable has a TDefaultDocument of A
3. When inserting the documents use Insert<A>

Note that the following two statements are not equivalent:

collection.Insert<A>(c);
collection.Insert(c);

In the first statement the nominalType is A and the actualType is C so
therefore a discriminator is written.

In the second statement the compiler infers the nominalType to be C by
inspecting the argument. In this case the nominalType and actualType
are equal so no discriminator is written.

Setting a RootClass has more to do with whether to serialize the
discriminator as an array or not. The nominalType vs actualType is a
better way to control whether a discriminator needs to be written.

Hope this helps. Please follow up if there are remaining questions.

Gabriel Duford

unread,
Mar 28, 2011, 3:34:12 PM3/28/11
to mongod...@googlegroups.com, Robert Stam
Ok, I got it, thanks a lot.
I was using "collection.Save(c)", instead of "collection.Save<A>(c)". The reasons are:
- I come from the samus driver, which uses the first form.
- I assumed that since "collection" is of type "MongoCollection<A>", it would not need to be told about the type in the Save method. I understand your explanation of the technical part (compiler inference), but can you give a quick explanation of the thinking/philosophy? Why isn't the generic type of the collection used to get the nominal type?
Good thing I got this while experimenting. I am taking notes about that kind of differences between samus' driver and the official one.
Thanks again,

gabriel

Robert Stam

unread,
Mar 28, 2011, 6:05:58 PM3/28/11
to mongodb-user
I see where you're coming from. I'm hoping that someone who hadn't
used a different driver before wouldn't have the same confusion.

The thinking about why the TDefaultDocument in
MongoCollection<TDefaultDocument> is not used as the nominalType is as
follows:

- Collections in MongoDB are schemaless
- So there is no such thing as a collection of <T>, strictly speaking
- Assuming TDefaultDocument is the nominal type for a Save or Insert,
while it coincidentally might have been the right thing in your case,
would be the wrong thing if some other type of document was being
written

If you look at where TDefaultDocument is used, it is only used for
reads. For all output operations the document type, if not explicitly
stated, is inferred from the actual variable provided. This is in
keeping with collections being schema free, where the schema for each
document stored *could* be different.

Gabriel Duford

unread,
Mar 28, 2011, 6:20:04 PM3/28/11
to mongod...@googlegroups.com, Robert Stam
Thank you very much for the explanation, Robert.
The way I saw the MongoCollection<T> was more like a typed accessor/handle on an untyped collection. An accessor through which everything would be typed by T. If I would have wanted to access documents that are of another type, I would have used a collection of a different T. If I would have wanted to access all the documents that may not be of a related type, I would have used a MongoCollection<BsonDocument>.
But your explanation is clear and logic: the type T is only used for reading. With that information, I can educate my team and hope they won't forget to specify the nominal type when saving/inserting.
Thanks again for your quick responses.

gabriel
Reply all
Reply to author
Forward
0 new messages