/// <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); }
/// <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); } }
/// <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(); }
/// <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); } } }
//--- 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); }