From a0a3c86107d593b14837099153613b9119e043cd Mon Sep 17 00:00:00 2001 From: Mike Woofter <108414937+mongoKart@users.noreply.github.com> Date: Thu, 30 Nov 2023 15:53:38 -0600 Subject: [PATCH] DOCSP-26552 - polymorphism (#143) (cherry picked from commit 139fe4787fc546510266479e598eb31c6d06997c) --- source/fundamentals/class-mapping.txt | 43 --- source/fundamentals/data-formats.txt | 4 +- .../data-formats/polymorphism.txt | 270 ++++++++++++++++++ 3 files changed, 273 insertions(+), 44 deletions(-) create mode 100644 source/fundamentals/data-formats/polymorphism.txt diff --git a/source/fundamentals/class-mapping.txt b/source/fundamentals/class-mapping.txt index 2822467e..eb9e9188 100644 --- a/source/fundamentals/class-mapping.txt +++ b/source/fundamentals/class-mapping.txt @@ -127,49 +127,6 @@ You can also ignore any extra elements when registering a class map: classMap.SetIgnoreExtraElements(true); }); -Using Class Discriminators -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can specify **discriminators** to help identify **polymorphic** classes that -are serialized to the same collection. Polymorphic classes are classes that -inherit properties and methods from a parent class. A discriminator is an -element that's added to a document to identify which class the document was -serialized from. - -You can specify a discriminator using the ``BsonDiscriminator`` attribute as -follows: - -.. code-block:: csharp - - [BsonDiscriminator("personClass")] - public class Person - { - public string Name { get; set; } - public int Age { get; set; } - public List Hobbies {get; set;} - } - -You can also specify a discriminator when registering a class map as follows: - -.. code-block:: csharp - - BsonClassMap.RegisterClassMap(classMap => - { - classMap.AutoMap(); - classMap.SetDiscriminator("personClass"); - }); - -In BSON, discriminators have the field name ``_t``. - -The following example shows how a document from the ``Person`` class with the -"personClass" discriminator appears in the collection after serialization: - -.. code-block:: json - - { "_id": "...", "_t": "personClass", "Name": "...", "Age": "...", "Hobbies": [...]} - -.. TODO: Link to page on polymorphism/discriminators - Mapping with Constructors ------------------------- diff --git a/source/fundamentals/data-formats.txt b/source/fundamentals/data-formats.txt index edb88b54..8dd0a16f 100644 --- a/source/fundamentals/data-formats.txt +++ b/source/fundamentals/data-formats.txt @@ -13,8 +13,10 @@ Data Formats /fundamentals/data-formats/poco /fundamentals/data-formats/guid-serialization /fundamentals/data-formats/serialization + /fundamentals/data-formats/polymorphism - :ref:`csharp-bson` - :ref:`csharp-poco` - :ref:`csharp-guids` -- :ref:`csharp-serialization` \ No newline at end of file +- :ref:`csharp-serialization` +- :ref:`csharp-polymorphism` \ No newline at end of file diff --git a/source/fundamentals/data-formats/polymorphism.txt b/source/fundamentals/data-formats/polymorphism.txt new file mode 100644 index 00000000..a9fc458e --- /dev/null +++ b/source/fundamentals/data-formats/polymorphism.txt @@ -0,0 +1,270 @@ +.. _csharp-polymorphism: + +=================== +Polymorphic Objects +=================== + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + +.. facet:: + :name: genre + :values: reference + +.. meta:: + :keywords: inheritance, child, parent, hierarchy, derived, base, serialize, deserialize, root + +Overview +-------- + +**Polymorphic** objects inherit properties and methods from one or more parent classes. +These objects require special mapping to ensure that the {+driver-short+} correctly +serializes them to and from BSON documents. + +This guide explains the following: + +- How to deserialize polymorphic types +- The discriminator conventions included with the {+driver-short+} +- How to create custom discriminator conventions + +The examples on this page use the following inheritance hierarchy: + +.. code-block:: csharp + + public class Animal + { + } + + public class Cat : Animal + { + } + + public class Dog : Animal + { + } + + public class Lion : Cat + { + } + + public class Tiger : Cat + { + } + +Deserialize Polymorphic Objects +------------------------------- + +Before the serializer can deserialize any polymorphic objects, you must document the +relationship of all classes in the inheritance hierarchy. + +If you're using the automapper to map your classes, apply the ``[BsonKnownTypes]`` +attribute to each base class in the hierarchy. Pass each class that directly inherits +from the base class as an argument. + +The following example shows how to apply the ``[BsonKnownTypes]`` attribute to +classes in the example ``Animal`` hierarchy: + +.. code-block:: csharp + :emphasize-lines: 1,6 + + [BsonKnownTypes(typeof(Cat), typeof(Dog))] + public class Animal + { + } + + [BsonKnownTypes(typeof(Lion), typeof(Tiger))] + public class Cat : Animal + { + } + + public class Dog : Animal + { + } + + public class Lion : Cat + { + } + + public class Tiger : Cat + { + } + +.. note:: Using BsonKnownTypes + + Apply the ``[BsonKnownTypes]`` attribute only to parent classes. Pass as arguments + only the types that *directly* inherit from the class, not all child classes in + the hierarchy. + +If you're creating a class map manually, call the +``BsonClassMap.RegisterClassMap()`` method for every class in the hierarchy, as shown +in the following example: + +.. code-block:: csharp + + BsonClassMap.RegisterClassMap(); + BsonClassMap.RegisterClassMap(); + BsonClassMap.RegisterClassMap(); + BsonClassMap.RegisterClassMap(); + BsonClassMap.RegisterClassMap(); + +.. tip:: Class Maps + + To learn more about mapping classes, see the :ref:`csharp-class-mapping` documentation. + +Use Discriminators +------------------ + +In MongoDB, a **discriminator** is a field added to a document to identify the class +to which the document deserializes. When a collection contains more than one type from a +single inheritance hierarchy, discriminators ensure that each +document deserializes to the right class. The {+driver-short+} stores the discriminator +value in a field named ``_t`` in the BSON document. Generally, ``_t`` is the second +field in the BSON document after ``_id``. + +**Discriminator conventions** define the value stored in the discriminator field. +In this section, you can learn about the discriminator conventions included +with the {+driver-short+} and how to create custom discriminator conventions. + +ScalarDiscriminatorConvention +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the {+driver-short+} uses the ``ScalarDiscriminatorConvention``. According +to this convention, the {+driver-short+} sets the value of the ``_t`` field to the name of +the class from which the document was serialized. + +Suppose you create an instance of the example ``Animal`` class and each of its +subclasses. If you serialize these objects to a single collection, the +{+driver-short+} applies the ``ScalarDiscriminatorConvention`` and the corresponding +BSON documents appear as follows: + +.. code-block:: json + :copyable: false + + { _id: ..., _t: "Animal", ... } + { _id: ..., _t: "Cat", ... } + { _id: ..., _t: "Dog", ... } + { _id: ..., _t: "Lion", ... } + { _id: ..., _t: "Tiger", ... } + +The ``ScalarDiscriminatorConvention`` uses concise discriminator values, but can be +difficult to run a query on. For example, to find all documents of type or subtype ``Cat``, +you must explicitly list each class you're looking for: + +.. code-block:: csharp + :copyable: true + + var query = coll.Aggregate().Match(a => a is Cat || a is Lion || a is Tiger); + +HierarchicalDiscriminatorConvention +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To simplify queries against your collection of polymorphic types, you can use the +``HierarchicalDiscriminatorConvention``. According to this convention, the value of ``_t`` +is an array of all classes in the inheritance hierarchy of the document's type. + +To use the ``HierarchicalDiscriminatorConvention``, label the base class of your +inheritance hierarchy as the root class. If you're using the automapper, +label the root class by applying the +``[BsonDiscriminatorAttribute]`` attribute to the class and passing ``RootClass = true`` +as an argument. The following code example labels the ``Animal`` class as the +root of the example inheritance hierarchy: + +.. code-block:: csharp + :emphasize-lines: 1 + + [BsonDiscriminator(RootClass = true)] + [BsonKnownTypes(typeof(Cat), typeof(Dog)] + public class Animal + { + } + +If you're creating a class map manually, call the ``SetIsRootClass()`` method and pass +``true`` as an argument when you register the class map for the root class. The following +code example registers class maps for all five example classes but labels only the +``Animal`` class as the root of the inheritance hierarchy: + +.. code-block:: csharp + :copyable: true + :emphasize-lines: 3 + + BsonClassMap.RegisterClassMap(classMap => { + classMap.AutoMap(); + classMap.SetIsRootClass(true); + }); + BsonClassMap.RegisterClassMap(); + BsonClassMap.RegisterClassMap(); + BsonClassMap.RegisterClassMap(); + BsonClassMap.RegisterClassMap(); + +Suppose you label the example ``Animal`` class as the root of the inheritance hierarchy, +and then create an instance of the ``Animal`` class and each of its +subclasses. If you serialize these objects to a single collection, the {+driver-short+} +applies the ``HierarchicalDiscriminatorConvention`` and the corresponding +BSON documents appear as follows: + +.. code-block:: javascript + + { _id: ..., _t: "Animal", ... } + { _id: ..., _t: ["Animal", "Cat"], ... } + { _id: ..., _t: ["Animal", "Dog"], ... } + { _id: ..., _t: ["Animal", "Cat", "Lion"], ... } + { _id: ..., _t: ["Animal", "Cat", "Tiger"], ... } + +.. important:: Root Class Discriminator + + Any document mapped to the root class still uses a string value for the discriminator + field. + +When using the ``HierarchicalDiscriminatorConvention``, you can search for all +documents of type or subtype ``Cat`` by using a single boolean condition, as shown in +the following example: + +.. code-block:: csharp + :copyable: true + + var query = coll.Aggregate().Match(a => a is Cat); + +Custom Discriminator Conventions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you're working with data that doesn't follow the conventions used by the +{+driver-short+}--for example, data inserted into MongoDB by another driver or object +mapper--you might need to use a different value for your discriminator field to +ensure your classes align with those conventions. + +If you're using the automapper, you can specify a custom value for a class's discriminator +field by applying the ``[BsonDiscriminator]`` attribute to the class and passing +the custom discriminator value as a string argument. The following code example +sets the value of the discriminator field for the ``Animal`` class to "myAnimalClass": + +.. code-block:: csharp + :emphasize-lines: 1 + + [BsonDiscriminator("myAnimalClass")] + public class Animal + { + } + +If you're creating a class map manually, call the ``SetDiscriminator()`` method and pass +the custom discriminator value as an argument when +you register the class map. The following code example sets the value of the +discriminator field for the ``Animal`` class to "myAnimalClass": + +.. code-block:: csharp + :emphasize-lines: 4 + + BsonClassMap.RegisterClassMap(classMap => + { + classMap.AutoMap(); + classMap.SetDiscriminator("myAnimalClass"); + }); + +An instance of the previous ``Animal`` class appears as follows after serialization: + +.. code-block:: json + :copyable: false + + { "_id": "...", "_t": "myAnimalClass"}