/// <summary>Null value is handled via [BsonIgnoreIfNull] attribute and is not expected here.</summary>
        public override TKey Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
        {
            // Read key as string in semicolon delimited format
            // with collection name followed by the equal sign
            // as prefix, for example:
            //
            // CollectionName=KeyElement1;KeyElement2
            string str = context.Reader.ReadString();

            // Confirm that collection name prefix matches
            // the expected collection name for the type
            // followed by the equal sign (=).
            string[] strTokens      = str.Split(new char[] { '=' }, 2);
            string   collectionName = DataTypeInfo.GetOrCreate(typeof(TKey)).GetCollectionName();

            if (strTokens.Length != 2 || strTokens[0] != collectionName)
            {
                throw new Exception(
                          $"Key {str} does not start from the expected " +
                          $"collection name {collectionName} followed by the equal sign (=).");
            }

            // Deserialize key from the part of the string
            // after the equal sign (=).
            string keyStr = strTokens[1];
            var    key    = new TKey();

            key.PopulateFrom(keyStr);
            return(key);
        }
        /// <summary>
        /// Write start document tags. This method
        /// should be called only once for the entire document.
        /// </summary>
        public void WriteStartDocument(string rootElementName)
        {
            // Check transition matrix
            if (currentState_ == TreeWriterState.Empty && elementStack_.Count == 0)
            {
                currentState_ = TreeWriterState.DocumentStarted;
            }
            else
            {
                throw new Exception(
                          $"A call to WriteStartDocument(...) must be the first call to the tree writer.");
            }

            // Get root XML element name using mapped final type of the object
            string rootName = currentDict_.GetType().Name;

            // Check that the name matches
            if (rootElementName != rootName)
            {
                throw new Exception(
                          $"Attempting to deserialize data for type {rootElementName} into type {rootName}.");
            }

            rootElementName_    = rootElementName;
            currentElementName_ = rootElementName;
            var currentDictInfoList = DataTypeInfo.GetOrCreate(currentDict_).DataElements;

            currentDictElements_ = new Dictionary <string, PropertyInfo>();
            foreach (var elementInfo in currentDictInfoList)
            {
                currentDictElements_.Add(elementInfo.Name, elementInfo);
            }
            currentArray_         = null;
            currentArrayItemType_ = null;
        }
        /// <summary>Null value is handled via [BsonIgnoreIfNull] attribute and is not expected here.</summary>
        public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, TKey value)
        {
            // Serialize key in semicolon delimited format,
            // with collection name followed by the equal sign
            // as prefix, for example:
            //
            // CollectionName=KeyElement1;KeyElement2
            string collectionName = DataTypeInfo.GetOrCreate(typeof(TKey)).GetCollectionName();
            string keyStr         = value.ToString();
            string str            = string.Join("=", collectionName, keyStr);

            context.Writer.WriteString(str);
        }
예제 #4
0
        /// <summary>
        /// Populate key elements by taking them from the matching
        /// elements of the argument record.
        /// </summary>
        public void PopulateFrom(TypedRecord <TKey, TRecord> record)
        {
            // Assign elements of the record to the matching elements
            // of the key. This will also make string representation
            // of the key return the proper value for the record.
            //
            // Get PropertyInfo arrays for TKey and TRecord
            var dataElementInfoDict = DataTypeInfo.GetOrCreate <TRecord>().DataElementDict;
            var keyElementInfoArray = DataTypeInfo.GetOrCreate <TKey>().DataElements;

            // Check that TRecord has the same or greater number of elements
            // as TKey (all elements of TKey must also be present in TRecord)
            if (dataElementInfoDict.Count < keyElementInfoArray.Length)
            {
                throw new Exception(
                          $"Record type {typeof(TRecord).Name} has fewer elements than key type {typeof(TKey).Name}.");
            }

            // Iterate over the key elements
            foreach (var keyElementInfo in keyElementInfoArray)
            {
                if (!dataElementInfoDict.TryGetValue(keyElementInfo.Name, out var dataElementInfo))
                {
                    throw new Exception(
                              $"Element {keyElementInfo.Name} of key type {typeof(TKey).Name} " +
                              $"is not found in the record type {typeof(TRecord).Name}.");
                }

                if (keyElementInfo.PropertyType != dataElementInfo.PropertyType)
                {
                    throw new Exception(
                              $"Element {typeof(TKey).Name} has type {keyElementInfo.PropertyType.Name} which does not " +
                              $"match the type {dataElementInfo.PropertyType.Name} of the corresponding element in the " +
                              $"record type {typeof(TRecord).Name}.");
                }

                // Read from the record and assign to the key
                object elementValue = dataElementInfo.GetValue(record);
                keyElementInfo.SetValue(this, elementValue);
            }
        }
예제 #5
0
        /// <summary>Serialize by writing into ITreeWriter.</summary>
        public void SerializeTo(ITreeWriter writer)
        {
            // Write start tag
            writer.WriteStartDict();

            // Iterate over the list of elements
            var innerElementInfoList = DataTypeInfo.GetOrCreate(this).DataElements;

            foreach (var innerElementInfo in innerElementInfoList)
            {
                // Get element name and value
                string innerElementName   = innerElementInfo.Name;
                object innterElementValue = innerElementInfo.GetValue(this);

                // Serialize based on type of element value
                switch (innterElementValue)
                {
                case null:
                    // Do not serialize null value
                    break;

                case string stringValue:
                case double doubleValue:
                case bool boolValue:
                case int intValue:
                case long longValue:
                case LocalDate dateValue:
                case LocalTime timeValue:
                case LocalMinute minuteValue:
                case LocalDateTime dateTimeValue:
                case Instant instantValue:
                case Enum enumValue:
                    // Embedded as string value
                    writer.WriteValueElement(innerElementName, innterElementValue);
                    break;

                case IEnumerable enumerableElement:
                    // Embedded enumerable such as array or list
                    enumerableElement.SerializeTo(innerElementName, writer);
                    break;

                case Data dataElement:
                    if (dataElement is Key)
                    {
                        // Embedded as string key
                        writer.WriteValueElement(innerElementName, innterElementValue);
                        break;
                    }
                    else
                    {
                        // Embedded as data
                        writer.WriteStartElement(innerElementName);
                        dataElement.SerializeTo(writer);
                        writer.WriteEndElement(innerElementName);
                    }
                    break;

                case TemporalId idElement:
                    // Do not serialize
                    break;

                default:
                    // Argument type is unsupported, error message
                    throw new Exception($"Element type {innerElementInfo.PropertyType} is not supported for tree serialization.");
                }
            }

            // Write end tag
            writer.WriteEndDict();
        }
예제 #6
0
        /// <summary>Deserialize from data in ITreeReader.</summary>
        public void DeserializeFrom(ITreeReader reader)
        {
            // Do nothing if the selected XML node is empty
            if (reader == null)
            {
                return;
            }

            // Iterate over the list of elements
            var elementInfoList = DataTypeInfo.GetOrCreate(this).DataElements;

            foreach (var elementInfo in elementInfoList)
            {
                // Get element name and type
                string elementName = elementInfo.Name;
                Type   elementType = elementInfo.PropertyType;

                // Get inner XML node, continue with next element if null
                ITreeReader innerXmlNode = reader.ReadElement(elementName);
                if (innerXmlNode == null)
                {
                    continue;
                }

                // First check for each of the supported value types
                if (elementType == typeof(string))
                {
                    string token = innerXmlNode.ReadValue();
                    elementInfo.SetValue(this, token);
                }
                else if (elementType == typeof(double) || elementType == typeof(double?))
                {
                    string token = innerXmlNode.ReadValue();
                    var    value = double.Parse(token);
                    elementInfo.SetValue(this, value);
                }
                else if (elementType == typeof(bool) || elementType == typeof(bool?))
                {
                    string token = innerXmlNode.ReadValue();
                    var    value = bool.Parse(token);
                    elementInfo.SetValue(this, value);
                }
                else if (elementType == typeof(int) || elementType == typeof(int?))
                {
                    string token = innerXmlNode.ReadValue();
                    var    value = int.Parse(token);
                    elementInfo.SetValue(this, value);
                }
                else if (elementType == typeof(long) || elementType == typeof(long?))
                {
                    string token = innerXmlNode.ReadValue();
                    var    value = long.Parse(token);
                    elementInfo.SetValue(this, value);
                }
                else if (elementType == typeof(LocalDate) || elementType == typeof(LocalDate?))
                {
                    string token = innerXmlNode.ReadValue();
                    var    value = LocalDateUtil.Parse(token);
                    elementInfo.SetValue(this, value);
                }
                else if (elementType == typeof(LocalTime) || elementType == typeof(LocalTime?))
                {
                    string token = innerXmlNode.ReadValue();
                    var    value = LocalTimeUtil.Parse(token);
                    elementInfo.SetValue(this, value);
                }
                else if (elementType == typeof(LocalMinute) || elementType == typeof(LocalMinute?))
                {
                    string token = innerXmlNode.ReadValue();
                    var    value = LocalMinuteUtil.Parse(token);
                    elementInfo.SetValue(this, value);
                }
                else if (elementType == typeof(LocalDateTime) || elementType == typeof(LocalDateTime?))
                {
                    string token = innerXmlNode.ReadValue();
                    var    value = LocalDateTimeUtil.Parse(token);
                    elementInfo.SetValue(this, value);
                }
                else if (elementType == typeof(Instant) || elementType == typeof(Instant?))
                {
                    string token = innerXmlNode.ReadValue();
                    var    value = InstantUtil.Parse(token);
                    elementInfo.SetValue(this, value);
                }
                else if (elementType.IsSubclassOf(typeof(Enum)))
                {
                    string token = innerXmlNode.ReadValue();
                    var    value = Enum.Parse(elementType, token);
                    elementInfo.SetValue(this, value);
                }
                else
                {
                    // If none of the supported atomic types match, use the activator
                    // to create and empty instance of a complex type and populate it
                    var element = Activator.CreateInstance(elementType);
                    switch (element)
                    {
                    case IList listElement:
                        listElement.DeserializeFrom(elementName, reader);
                        break;

                    case Data dataElement:
                        var keyElement = dataElement as Key;
                        if (keyElement != null)
                        {
                            // Deserialize key from value node containing semicolon delimited string
                            string token = innerXmlNode.ReadValue();
                            // Parse semicolon delimited string to populate key elements
                            keyElement.PopulateFrom(token);
                        }
                        else
                        {
                            // Deserialize embedded data object from the contents of inner XML node
                            dataElement.DeserializeFrom(innerXmlNode);
                        }
                        break;

                    case TemporalId idElement:
                        // Do not serialize
                        break;

                    default:
                        // Error message if the type does not match any of the value or reference types
                        throw new Exception($"Serialization is not supported for type {elementType}.");
                    }

                    // Assign the populated key to the property
                    elementInfo.SetValue(this, element);
                }
            }
        }
예제 #7
0
        //--- PRIVATE

        /// <summary>
        /// Populate key elements from an array of tokens starting
        /// at the specified token index. Elements that are themselves
        /// keys may use more than one token.
        ///
        /// This method returns the index of the first unused token.
        /// The returned value is the same as the length of the tokens
        /// array if all tokens are used.
        ///
        /// If key AKey has two elements, B and C, where
        ///
        /// * B has type BKey which has two string elements, and
        /// * C has type string,
        ///
        /// the semicolon delimited key has the following format:
        ///
        /// BToken1;BToken2;CToken
        ///
        /// To avoid serialization format uncertainty, key elements
        /// can have any atomic type except Double.
        /// </summary>
        private int PopulateFrom(string[] tokens, int tokenIndex)
        {
            // Get key elements using reflection
            var elementInfoArray = DataTypeInfo.GetOrCreate(this).DataElements;

            // If singleton is detected process it separately, then exit
            if (elementInfoArray.Length == 0)
            {
                // Check that string key is empty
                if (tokens.Length != 1 || tokens[0] != String.Empty)
                {
                    throw new Exception($"Type {GetType()} has key {string.Join(";", tokens)} while " +
                                        $"for a singleton the key must be an empty string (String.Empty). " +
                                        $"Singleton key is a key that has no key elements.");
                }

                // Return the length of empty key which consists of one (empty) token
                return(1);
            }

            // Check that there are enough remaining tokens in the key for each key element
            if (tokens.Length - tokenIndex < elementInfoArray.Length)
            {
                throw new Exception(
                          $"Key of type {GetType().Name} requires at least {elementInfoArray.Length} elements " +
                          $"{String.Join(";", elementInfoArray.Select(p => p.Name).ToArray())} while there are " +
                          $"only {tokens.Length - tokenIndex} remaining key tokens: {string.Join(";", tokens)}.");
            }

            // Iterate over element info elements, advancing tokenIndex by the required
            // number of tokens for each element. In case of embedded keys, the value of
            // tokenIndex is advanced by the recursive call to InitFromTokens method
            // of the embedded key.
            foreach (var elementInfo in elementInfoArray)
            {
                // Get element type
                Type elementType = elementInfo.PropertyType;

                // Convert string token to value depending on elementType
                if (elementType == typeof(string))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    string token = tokens[tokenIndex++];
                    elementInfo.SetValue(this, token);
                }
                else if (elementType == typeof(double) || elementType == typeof(double?))
                {
                    throw new Exception(
                              $"Key element {elementInfo.Name} has type Double. Elements of this type " +
                              $"cannot be part of key due to serialization format uncertainty.");
                }
                else if (elementType == typeof(bool) || elementType == typeof(bool?))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    string token      = tokens[tokenIndex++];
                    bool   tokenValue = bool.Parse(token);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (elementType == typeof(int) || elementType == typeof(int?))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    string token      = tokens[tokenIndex++];
                    int    tokenValue = int.Parse(token);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (elementType == typeof(long) || elementType == typeof(long?))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    string token      = tokens[tokenIndex++];
                    long   tokenValue = long.Parse(token);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (elementType == typeof(LocalDate) || elementType == typeof(LocalDate?))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    // Inside the key, LocalDate is represented as readable int in
                    // non-delimited yyyymmdd format, not as delimited ISO string.
                    //
                    // First parse the string to int, then convert int to LocalDate.
                    string token = tokens[tokenIndex++];
                    if (!Int32.TryParse(token, out int isoInt))
                    {
                        throw new Exception(
                                  $"Element {elementInfo.Name} of key type {GetType().Name} has type LocalDate and value {token} " +
                                  $"that cannot be converted to readable int in non-delimited yyyymmdd format.");
                    }

                    LocalDate tokenValue = LocalDateUtil.FromIsoInt(isoInt);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (elementType == typeof(LocalTime) || elementType == typeof(LocalTime?))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    // Inside the key, LocalTime is represented as readable int in
                    // non-delimited hhmmssfff format, not as delimited ISO string.
                    //
                    // First parse the string to int, then convert int to LocalTime.
                    string token = tokens[tokenIndex++];
                    if (!Int32.TryParse(token, out int isoInt))
                    {
                        throw new Exception(
                                  $"Element {elementInfo.Name} of key type {GetType().Name} has type LocalTime and value {token} " +
                                  $"that cannot be converted to readable int in non-delimited hhmmssfff format.");
                    }

                    LocalTime tokenValue = LocalTimeUtil.FromIsoInt(isoInt);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (elementType == typeof(LocalMinute) || elementType == typeof(LocalMinute?))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    // Inside the key, LocalMinute is represented as readable int in
                    // non-delimited hhmm format, not as delimited ISO string.
                    //
                    // First parse the string to int, then convert int to LocalTime.
                    string token = tokens[tokenIndex++];
                    if (!Int32.TryParse(token, out int isoInt))
                    {
                        throw new Exception(
                                  $"Element {elementInfo.Name} of key type {GetType().Name} has type LocalMinute and value {token} " +
                                  $"that cannot be converted to readable int in non-delimited hhmm format.");
                    }

                    LocalMinute tokenValue = LocalMinuteUtil.FromIsoInt(isoInt);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (elementType == typeof(LocalDateTime) || elementType == typeof(LocalDateTime?))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    // Inside the key, LocalDateTime is represented as readable long in
                    // non-delimited yyyymmddhhmmssfff format, not as delimited ISO string.
                    //
                    // First parse the string to long, then convert int to LocalDateTime.
                    string token = tokens[tokenIndex++];
                    if (!Int64.TryParse(token, out long isoLong))
                    {
                        throw new Exception(
                                  $"Element {elementInfo.Name} of key type {GetType().Name} has type LocalDateTime and value {token} " +
                                  $"that cannot be converted to readable long in non-delimited yyyymmddhhmmssfff format.");
                    }

                    LocalDateTime tokenValue = LocalDateTimeUtil.FromIsoLong(isoLong);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (elementType == typeof(Instant) || elementType == typeof(Instant?))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    // Inside the key, Instant is represented as readable long in
                    // non-delimited yyyymmddhhmmssfff format, not as delimited ISO string.
                    //
                    // First parse the string to long, then convert int to Instant.
                    string token = tokens[tokenIndex++];
                    if (!Int64.TryParse(token, out long isoLong))
                    {
                        throw new Exception(
                                  $"Element {elementInfo.Name} of key type {GetType().Name} has type Instant and value {token} " +
                                  $"that cannot be converted to readable long in non-delimited yyyymmddhhmmssfff format.");
                    }

                    Instant tokenValue = InstantUtil.FromIsoLong(isoLong);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (elementType == typeof(TemporalId) || elementType == typeof(TemporalId?))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    string     token      = tokens[tokenIndex++];
                    TemporalId tokenValue = TemporalId.Parse(token);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (elementType.BaseType == typeof(Enum)) // TODO Support nullable Enum in key
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    string token      = tokens[tokenIndex++];
                    object tokenValue = Enum.Parse(elementType, token);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (typeof(Key).IsAssignableFrom(elementType))
                {
                    Key keyElement = (Key)Activator.CreateInstance(elementType);
                    tokenIndex = keyElement.PopulateFrom(tokens, tokenIndex);
                    elementInfo.SetValue(this, keyElement);
                }
                else
                {
                    // Field type is unsupported for a key, error message
                    throw new Exception(
                              $"Element {elementInfo.Name} of key type {GetType().Name} has type {elementType} that " +
                              $"is not one of the supported key element types. Available key element types are " +
                              $"string, bool, int, long, LocalDate, LocalTime, LocalMinute, LocalDateTime, Instant, Enum, or Key.");
                }
            }

            return(tokenIndex);
        }
        /// <summary>
        /// Write dictionary start tag. A call to this method
        /// must follow WriteStartElement(...) or WriteStartArrayItem().
        /// </summary>
        public void WriteStartDict()
        {
            // Push state before defining dictionary state
            PushState();

            // Check state transition matrix
            if (currentState_ == TreeWriterState.DocumentStarted)
            {
                currentState_ = TreeWriterState.DictStarted;

                // Return if this call follows StartDocument, all setup is done in StartDocument
                return;
            }
            else if (currentState_ == TreeWriterState.ElementStarted)
            {
                currentState_ = TreeWriterState.DictStarted;
            }
            else if (currentState_ == TreeWriterState.ArrayItemStarted)
            {
                currentState_ = TreeWriterState.DictArrayItemStarted;
            }
            else
            {
                throw new Exception(
                          $"A call to WriteStartDict() must follow WriteStartElement(...) or WriteStartArrayItem().");
            }

            // Set dictionary info
            Type createdDictType = null;

            if (currentArray_ != null)
            {
                createdDictType = currentArrayItemType_;
            }
            else if (currentDict_ != null)
            {
                createdDictType = currentElementInfo_.PropertyType;
            }
            else
            {
                throw new Exception($"Value can only be added to a dictionary or array.");
            }

            object createdDictObj = Activator.CreateInstance(createdDictType);

            if (!(createdDictObj is Data)) // TODO Also support native dictionaries
            {
                string className = currentElementInfo_.PropertyType.Name;
                throw new Exception(
                          $"Element {currentElementInfo_.Name} of type {className} does not implement Data.");
            }

            var createdDict = (Data)createdDictObj;

            // Add to array or dictionary, depending on what we are inside of
            if (currentArray_ != null)
            {
                currentArray_[currentArray_.Count - 1] = createdDict;
            }
            else if (currentDict_ != null)
            {
                currentElementInfo_.SetValue(currentDict_, createdDict);
            }
            else
            {
                throw new Exception($"Value can only be added to a dictionary or array.");
            }

            currentDict_ = (Data)createdDict;
            var currentDictInfoList = DataTypeInfo.GetOrCreate(createdDictType).DataElements;

            currentDictElements_ = new Dictionary <string, PropertyInfo>();
            foreach (var elementInfo in currentDictInfoList)
            {
                currentDictElements_.Add(elementInfo.Name, elementInfo);
            }
            currentArray_         = null;
            currentArrayItemType_ = null;
        }
        //--- 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);
        }