[CSHARP-87] Inheritance Created: 27/Oct/10  Updated: 02/Apr/15  Resolved: 05/Nov/10

Status: Closed
Project: C# Driver
Component/s: None
Affects Version/s: None
Fix Version/s: 0.7

Type: Improvement Priority: Major - P3
Reporter: Craig Wilson Assignee: Robert Stam
Resolution: Done Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified


 Description   

Currently, the discriminator is only going to get published without referencing the base class' discriminator. This provides certain problems when a 3 level hierarchy is setup as below:

Animal -> (no discriminator)

  • Cat -> "Cat"
  • Tiger -> "Tiger"
  • Lion -> "Lion"
  • Dog -> "Dog"
  • Wolf -> "Wolf"

Currently, I cannot get all the Cats without a lot of work. In instead, we maintain the hierarchy of the classes in the discriminator, then we can search for

{ _t: "Cat" }

and get back Tigers, Lions, and SuperLions.

Animal -> (no discriminator)

  • Cat -> "Cat"
  • Tiger -> ["Cat", "Tiger"]
  • Lion -> ["Cat", "Lion"]
    ~ SuperLion -> ["Cat", "Lion", "SuperLion"]
  • Dog -> "Dog"
  • Wolf -> ["Dog, "Wolf"]

In addition, this change would be in line with the current way inheritance is handled in the mongodb-csharp driver. You can view the wiki link here: http://github.com/mongodb-csharp/mongodb-csharp/wiki/Inheritance



 Comments   
Comment by Robert Stam [ 05/Nov/10 ]

Fixed previously. Closing the JIRA ticket now.

Comment by Robert Stam [ 03/Nov/10 ]

Actually, as it turns out you don't need any initialization code when using attributes, as long as the type is mentioned in the Find (or other deserialization) method.

// this version requires no initialization code
var animal = collection.FindOneAs<Animal>();

// requires that RegisterClassMap<Animal> be called
var animal = collection.FindOneAs<object>();

Zero initialization code, that's pretty cool (as long as you're willing to use attributes).

Comment by Robert Stam [ 03/Nov/10 ]

I hope the way I'm doing it actually is easier from a user's perspective, if not we'll keep refining it. I've just committed the latest work on this, including some unit tests using the Animal class hierarchy from your example. I set up two separate unit tests: in one I use attributes to identify the root class and the known subclasses, in the other I use no attributes and initialize the class maps with code alone.

Here's what the initialization code looks like when attributes are used:

static AnimalHierarchyWithAttributesTests()

{ BsonClassMap.RegisterClassMap<Animal>(); }

and here's what the initialization code looks like when attributes are not used:

static AnimalHierarchyWithoutAttributesTests() {
BsonClassMap.RegisterClassMap<Animal>(cm =>

{ cm.AutoMap(); cm.SetIsRootClass(true); }

);
BsonClassMap.RegisterClassMap<Bear>();
BsonClassMap.RegisterClassMap<Tiger>();
BsonClassMap.RegisterClassMap<Lion>();
}

I think either way turned out to be rather simple for end users to use.

I'll leave this JIRA issue open for a bit longer in case we decide to make further changes based on feedback.

Thanks so much for all the feedback you have been providing. You probably don't realize how helpful it has been!

Comment by Craig Wilson [ 02/Nov/10 ]

It may be easier to identify the root in the way you described, but how will you identify the subclasses? 1) class A comes in and needs to be mapped, 2) you loop through all the registered classes to find out if any of them are in the inheritance chain of class A

The downside here is that you still have to require all classes depending on discriminators for re-hydration to be registered at application startup so that the discriminators are known prior to querying.

So, 1), we haven't gained anything from a user's perspective and 2), I think you've made your life more difficult when trying to figure out if something is a root class or a subclass.

Like I said, I personally don't care how it is accomplished. I'm just giving an opinion on how we did this is another driver and the thought process we went through there.

Comment by Robert Stam [ 02/Nov/10 ]

I didn't mean to imply that the convention you described doesn't provide a way to identify the root. I guess I was just point out the obvious: IF you can't identify the root you don't know where to stop. So there could be many ways to identify the root. I'm proposing that instead of a convention that identifies all the subclasses, it seems easier to identify the root directly.

Comment by Craig Wilson [ 02/Nov/10 ]

Aaahhhh, but we do know where to stop. We keep going up the type.BaseType until it isn't a subclass any longer. That is then our root.

Good point on including the discriminator. I personally don't use collections for varied document types (other than inheritance related), so I don't think about that.

Comment by Robert Stam [ 02/Nov/10 ]

Here's my current thinking: there's no point in serializing a hierarchical set of discriminators if you don't know where to stop, so therefore we must designate a particular class as the root class of a hierarchy. Furthermore, if we have marked a class as the root of a hierarchy that probably implies that we want hierarchical discriminators, so we don't need a separate way to say that. So I think it would look like this:

[BsonDiscriminator(RootClass = true)]
public class Animal

{ ... }

And of course a matching way to do it without attributes. But I don't think this is a convention, we're just identifying classes for which we want hierarchical discriminators.

Another minor detail: I think "Animal" should be included in the array of discriminators. So for example, the discriminator for a Lion would be ["Animal", "Cat", "Lion"]. The reason I say this is that you might find yourself querying a collection that has all kinds of documents (not just Animals of various sorts), and having "Animal" in the list lets you query for all the Animals.

Comment by Craig Wilson [ 02/Nov/10 ]

Yes. But, chances are that this is a far and few between circumstance. If it is like this, surely it wouldn't be inlined, but instead a static function somewhere to handle the decision.

Comment by Robert Stam [ 02/Nov/10 ]

What if Animal is only one of dozens of hierarchy roots? Do you just check against them all?

t => t.IsSubClassOf(typeof(Animal)) || t.IsSubClassOf(typeof(SomeRoot) || t.IsSubClassOf(SomeOtherRoot) || ...

Comment by Craig Wilson [ 02/Nov/10 ]

That's awesome. Regarding how you know... We(mongodb-csharp) have a "convention" that is basically a Func<Type, bool> that tells us if the type is a subclass or not.

So, profile.SetSubClassConvention(new DelegateSubclassConvention(t => t.IsSubClassOf(typeof(Animal)))); //or something like this

This lets us know that anything that is a subclass of Animal is a subclass and requires a discriminator. Otherwise, map it as a root.So , the rub here is that everything is a "hierarchy root" unless it's been specifically marked as a subclass.

In actuality, re-hydrating is actually the problem with discriminators. Anything that needs a discriminator (so, all subclasses) have to be explicitly mapped at application startup. Otherwise, the discriminator hasn't been registered and you won't know what type to re-hydrate. NoRM persists the .NET type, but, as I mentioned before, this is a pain to deal with when using more than just the NoRM driver (the ruby driver for instance). So, basically, anyone using subclasses needs to let you know about them when the application starts. Simple rule, simple process.

Comment by Robert Stam [ 02/Nov/10 ]

I almost got this working. Tiny details remain: how to know when to use hierarchical discriminators? and how to know where the hierarchy is rooted? Maybe both questions have the same answer: if a class is tagged as a hierarchical root (either by an attribute or by a Set method of some sort) that signals that subclasses should use hierarchical discriminators.

I went with the order in your original suggestion. I think leaving the order that way will help with migration of existing data.

I added a DiscriminatorConventions. Others will be able to use custom discriminator conventions if they need to inter-operate with existing data that uses some other kind of conventions.

Comment by Craig Wilson [ 31/Oct/10 ]

That'll work as well. Only persistence (saving) will need to use the array form. Querying will still only use the single discriminator for the specific type of document (s) being requested. MongoDB made that easy because of how it handles querying arrays.

Comment by Robert Stam [ 31/Oct/10 ]

Would it make any sense to reverse the order of the discriminators?

e.g. ["SuperLion", "Lion", "Cat"]

That way the deserializer would just take the first discriminator rather than the last.

Either way works of course, I'm just wondering which way is more "logical" or "natural"?

Comment by Robert Stam [ 28/Oct/10 ]

I think this makes sense. It makes the discriminator use slightly more space, but not much and provides valuable benefits.

Generated at Wed Feb 07 21:35:45 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.