Пример #1
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);
                }
            }
        }
        /// <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;
        }
Пример #3
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);
        }
        //--- 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);
        }