/// <summary>
        /// Create from collection for the base type Record, the lookup
        /// list of datasets, and IOrderedQueryable(TRecord).
        ///
        /// This constructor is private and is intended for use by the
        /// implementation of this class only.
        /// </summary>
        private TemporalMongoQuery(TemporalMongoCollection <TRecord> collection, TemporalId loadFrom, IOrderedQueryable <TRecord> orderedQueryable)
        {
            if (orderedQueryable == null)
            {
                throw new Exception(
                          "Attempting to create a query from a null orderedQueryable.");
            }

            collection_       = collection;
            loadFrom_         = loadFrom;
            orderedQueryable_ = orderedQueryable;
        }
        //--- CONSTRUCTORS

        /// <summary>
        /// Create query from collection and dataset.
        /// </summary>
        public TemporalMongoQuery(TemporalMongoCollection <TRecord> collection, TemporalId loadFrom)
        {
            collection_ = collection;
            loadFrom_   = loadFrom;

            // Create queryable from typed collection rather than base
            // collection so LINQ queries can be applied to properties
            // of the generic parameter type TRecord
            var queryable = collection_.TypedCollection.AsQueryable();

            // Without explicitly applying OfType, the match for _t
            // may or may not be added. This step ensures the match
            // for _t exists and is the first stage in the aggregation
            // pipeline
            queryable_ = queryable.OfType <TRecord>();
        }
        //--- PRIVATE

        /// <summary>
        /// Returned object holds two collection references - one for the base
        /// type of all records and the other for the record type specified
        /// as generic parameter.
        ///
        /// The need to hold two collection arises from the requirement
        /// that query for a derived type takes into account that another
        /// record with the same key and later dataset or object timestamp
        /// may exist. For this reason, the typed collection is used for
        /// LINQ constraints and base collection is used to iterate over
        /// objects.
        ///
        /// This method also creates indices if they do not exist. The
        /// two default indices are always created:  one for optimizing
        /// loading by key and the other by query.
        ///
        /// Additional indices may be created using class attribute
        /// [IndexElements] for further performance optimization.
        /// </summary>
        private TemporalMongoCollection <TRecord> GetOrCreateCollection <TRecord>()
            where TRecord : Record
        {
            // Check if collection object has already been cached
            // for this type and return cached result if found
            if (collectionDict_.TryGetValue(typeof(TRecord), out object collectionObj))
            {
                var cachedResult = collectionObj.CastTo <TemporalMongoCollection <TRecord> >();
                return(cachedResult);
            }

            // Check that hierarchical discriminator convention is set for TRecord
            var discriminatorConvention = BsonSerializer.LookupDiscriminatorConvention(typeof(TRecord));

            if (!discriminatorConvention.Is <HierarchicalDiscriminatorConvention>())
            {
                throw new Exception(
                          $"Hierarchical discriminator convention is not set for type {typeof(TRecord).Name}. " +
                          $"The convention should have been set set in the static constructor of " +
                          $"MongoDataSource");
            }

            // Collection name is root class name of the record without prefix
            string collectionName = DataTypeInfo.GetOrCreate <TRecord>().GetCollectionName();

            // Get interfaces to base and typed collections for the same name
            var baseCollection  = Db.GetCollection <Record>(collectionName);
            var typedCollection = Db.GetCollection <TRecord>(collectionName);

            //--- Load standard index types

            // Each data type has an index for optimized loading by key.
            // This index consists of Key in ascending order, followed by
            // DataSet and ID in descending order.
            var loadIndexKeys = Builders <TRecord> .IndexKeys
                                .Ascending(new StringFieldDefinition <TRecord>("_key"))      // .Key
                                .Descending(new StringFieldDefinition <TRecord>("_dataset")) // .DataSet
                                .Descending(new StringFieldDefinition <TRecord>("_id"));     // .Id

            // Use index definition convention to specify the index name
            var loadIndexName  = "Key-DataSet-Id";
            var loadIndexModel = new CreateIndexModel <TRecord>(loadIndexKeys, new CreateIndexOptions {
                Name = loadIndexName
            });

            typedCollection.Indexes.CreateOne(loadIndexModel);

            //--- Load custom index types

            // Additional indices are provided using IndexAttribute for the class.
            // Get a sorted dictionary of (definition, name) pairs
            // for the inheritance chain of the specified type.
            var indexDict = IndexElementsAttribute.GetAttributesDict <TRecord>();

            // Iterate over the dictionary to define the index
            foreach (var indexInfo in indexDict)
            {
                string indexDefinition = indexInfo.Key;
                string indexName       = indexInfo.Value;

                // Parse index definition to get a list of (ElementName,SortOrder) tuples
                List <(string, int)> indexTokens = IndexElementsAttribute.ParseDefinition <TRecord>(indexDefinition);

                var indexKeysBuilder = Builders <TRecord> .IndexKeys;
                IndexKeysDefinition <TRecord> indexKeys = null;

                // Iterate over (ElementName,SortOrder) tuples
                foreach (var indexToken in indexTokens)
                {
                    (string elementName, int sortOrder) = indexToken;

                    if (indexKeys == null)
                    {
                        // Create from builder for the first element
                        if (sortOrder == 1)
                        {
                            indexKeys = indexKeysBuilder.Ascending(new StringFieldDefinition <TRecord>(elementName));
                        }
                        else if (sortOrder == -1)
                        {
                            indexKeys = indexKeysBuilder.Descending(new StringFieldDefinition <TRecord>(elementName));
                        }
                        else
                        {
                            throw new Exception("Sort order must be 1 or -1.");
                        }
                    }
                    else
                    {
                        // Chain to the previous list of index keys for the remaining elements
                        if (sortOrder == 1)
                        {
                            indexKeys = indexKeys.Ascending(new StringFieldDefinition <TRecord>(elementName));
                        }
                        else if (sortOrder == -1)
                        {
                            indexKeys = indexKeys.Descending(new StringFieldDefinition <TRecord>(elementName));
                        }
                        else
                        {
                            throw new Exception("Sort order must be 1 or -1.");
                        }
                    }
                }

                if (indexName == null)
                {
                    throw new Exception("Index name cannot be null.");
                }
                var indexModel = new CreateIndexModel <TRecord>(indexKeys, new CreateIndexOptions {
                    Name = indexName
                });

                // Add to indexes for the collection
                typedCollection.Indexes.CreateOne(indexModel);
            }

            // Create result that holds both base and typed collections
            TemporalMongoCollection <TRecord> result = new TemporalMongoCollection <TRecord>(this, baseCollection, typedCollection);

            // Add the result to the collection dictionary and return
            collectionDict_.TryAdd(typeof(TRecord), result);
            return(result);
        }