using Edge.Core.Parser.BinaryParser.Attributes; using Edge.Core.Parser.BinaryParser.MessageEntity; using Edge.Core.Parser.BinaryParser.Util; using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace Edge.Core.Parser.BinaryParser { public abstract class ParserBase : IMessageParser { protected static NLog.Logger logger = NLog.LogManager.LoadConfiguration("nlog.config").GetLogger("Communicator"); private int debug_ConvertToObject_depth = 0; private IMessageTemplateLookup templateLookup; /// /// ongoing processing raw data array, make 2 of this array is for thread safe /// private byte[] processingRawDataUsedInSerialize = null; /// /// ongoing processing raw data array, make 2 of this array is for thread safe /// private byte[] processingRawDataUsedInDeserialize = null; private int positiveIndexAccu = 0; /// /// Fired on when bytes incoming and will start to convert to a object, but not started yet. /// public event EventHandler> Deserializing; /// /// Fired on when bytes incoming and started to convert to a object, and finished already. /// public event EventHandler> Deserialized; public event EventHandler> Serializing; public event EventHandler> Serialized; /// /// Fired on a Format Attibute marked field will starting to convert to bytes, but not started yet. /// string in ParsingEventArg is the field name, byte[] should always null here since not started. /// public event EventHandler> FieldSerializing; /// /// Fired on a Format Attibute marked field started to convert to bytes, and finished already. /// string in ParsingEventArg is the field name, byte[] should have values for this already serialized field. /// public event EventHandler> FieldSerialized; protected ParserBase(IMessageTemplateLookup templateLookup) { this.templateLookup = templateLookup; } protected ParserBase() : this(null) { } /// /// There's a build-in check in Deserialize process, if all fields in a template had been filled, but still /// some extra bytes left, an exception will throw, this must be the error in raw bytes. /// But you can. /// public bool IgnoreExtraTailBytesInDeserialize { get; set; } /// /// Deserialize a byte[] into a Message entity, the `TemplateLookup` must specified firstly via constructor. /// /// /// public virtual MessageTemplateBase Deserialize(byte[] rawData) { //if (rawData.Length < 5) //{ // return default(MessageTemplateBase); //} if (this.templateLookup == null) throw new ArgumentNullException("Must specify a 'TemplateLookup' to start Deserialize"); return this.Deserialize(rawData, templateLookup.GetMessageTemplateByRawBytes(rawData)); } public virtual MessageTemplateBase Deserialize(byte[] rawData, MessageTemplateBase withTemplate) { this.debug_ConvertToObject_depth = 0; var debug = rawData.ToHexLogString(); var safeEvent = this.Deserializing; safeEvent?.Invoke(this, new ParsingEventArg() { From = rawData, To = default(MessageTemplateBase) }); //MessageTemplateBase template = templateLookup.GetMessageTemplateByRawBytes(rawData); this.processingRawDataUsedInDeserialize = rawData; this.ConvertToObject(withTemplate, this.processingRawDataUsedInDeserialize); if (this.processingRawDataUsedInDeserialize.Length > 0) { if (!IgnoreExtraTailBytesInDeserialize) throw new FormatException("Unexpected Element Deserialize error occured(additional tail existed)"); } #region if message level custom validation defined? then execute it Exception customValidationException; if ((customValidationException = withTemplate.Validate()) != null) { throw customValidationException; } #endregion var safeDeserializedEvent = this.Deserialized; safeDeserializedEvent?.Invoke(this, new DeserializeEventArg() { From = rawData, Extra = this.processingRawDataUsedInDeserialize, To = withTemplate }); return withTemplate; } public virtual byte[] Serialize(MessageTemplateBase message) { #region if message level custom validation defined? then execute it Exception customValidationException; if ((customValidationException = message?.Validate()) != null) { throw customValidationException; } #endregion var safeEvent = this.Serializing; safeEvent?.Invoke(this, new ParsingEventArg() { From = message, To = null }); this.processingRawDataUsedInSerialize = new byte[0]; this.positiveIndexAccu = 0; var content = this.ConvertToBytes(message); safeEvent = this.Serialized; safeEvent?.Invoke(this, new ParsingEventArg() { From = message, To = content }); return content; } private void ConvertToObject(object elementInstance, byte[] rawData) { //if (++this.debug_ConvertToObject_depth >= 6) logger.Info("debug_ConvertToObject_depth is " + this.debug_ConvertToObject_depth + ": 0x" + rawData.ToHexLogString()); if (rawData.Length == 0 || elementInstance == null) { return; } else { // Only processing Format related Attribute marked properties, and order by its Index var targetPropertyList = elementInstance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .Where(p => p.GetCustomAttributes(typeof(AttributeBase), true).Length > 0) .OrderBy(info => ((AttributeBase)(info.GetCustomAttributes(typeof(AttributeBase), true)[0])).Index).ToList(); foreach (var propertyInfo in targetPropertyList) { // normal plain Property if (propertyInfo.GetCustomAttributes(typeof(EnumerableFormatAttribute), true).Length == 0) { int currentElementLength = -1; var format = (FormatAttribute)propertyInfo.GetCustomAttributes(typeof(FormatAttribute), true)[0]; if (!string.IsNullOrEmpty(format.LengthOrCountLink)) { currentElementLength = int.Parse( elementInstance.GetType().GetProperty(format.LengthOrCountLink).GetValue(elementInstance, null).ToString()); if (!string.IsNullOrEmpty(format.LengthOrCountLinkExpression)) { currentElementLength += ParseLengthOrCountLinkExpression(format.LengthOrCountLinkExpression); } } else { currentElementLength = format.FixedLengthOrCount; } // check if the remaining raw data bytes are enough to fill the current property if (this.processingRawDataUsedInDeserialize.Count() < currentElementLength) { throw new ArgumentException( string.Format("ConvertToObject Error: The remaining raw data bytes are insufficient to fill the current property '{0}'", propertyInfo.Name)); } this.FillPropertyData(format.EncodingType, propertyInfo, elementInstance, this.processingRawDataUsedInDeserialize.Take(currentElementLength).ToArray()); #region validate the range for Range Attribute marked field. // Validate the range, once failed, throw a exception var rangeValidator = propertyInfo.GetCustomAttributes(typeof(RangeAttribute), true).Length > 0 ? propertyInfo.GetCustomAttributes(typeof(RangeAttribute), true)[0] : null; if (rangeValidator != null && !((RangeAttribute)rangeValidator).IsValid(propertyInfo.GetValue(elementInstance, null))) { throw new ArgumentException(((RangeAttribute)rangeValidator).ErrorMessage); } #endregion this.processingRawDataUsedInDeserialize = this.processingRawDataUsedInDeserialize.Skip(currentElementLength).ToArray(); } // the IList Property else { bool isCascadedList = false; var listFormat = (EnumerableFormatAttribute)propertyInfo.GetCustomAttributes(typeof(EnumerableFormatAttribute), true)[0]; int? listCount = null; if (!string.IsNullOrEmpty(listFormat.LengthOrCountLink) && listFormat.LengthOrCountLink.ToLower() == "%cascade") { // never know the end of the data, it's a flow, so set a big value. listCount = this.processingRawDataUsedInDeserialize.Length; listCount += ParseLengthOrCountLinkExpression(listFormat.LengthOrCountLinkExpression); isCascadedList = true; } else if (!string.IsNullOrEmpty(listFormat.LengthOrCountLink)) { var linkPropertyValue = elementInstance.GetType().GetProperty(listFormat.LengthOrCountLink).GetValue(elementInstance, null); if (linkPropertyValue is Enum) { listCount = (int)linkPropertyValue; } else { listCount = int.Parse(linkPropertyValue.ToString()); } if (!string.IsNullOrEmpty(listFormat.LengthOrCountLinkExpression)) { listCount += ParseLengthOrCountLinkExpression(listFormat.LengthOrCountLinkExpression); } } else if (listFormat.FixedLengthOrCount > 0) { listCount = listFormat.FixedLengthOrCount; } else { throw new FormatException("Can't resolve the count number for IList property: " + propertyInfo.Name); } IList list = null; // since it's CascadedList, then probably there's no data anymore, it's a empty list, no need to do any parsing. if (this.processingRawDataUsedInDeserialize.Count() == 0 && isCascadedList) { } else { list = (IList)Activator.CreateInstance(propertyInfo.PropertyType); var genericArg = list.GetType().GetGenericArguments()[0]; for (int i = 0; i < listCount; i++) { if (genericArg.IsPrimitive) { var subPrimitiveInstance = Activator.CreateInstance(genericArg); // assume primitive always take 1 byte, since we only use int and byte. need refine future. subPrimitiveInstance = this.processingRawDataUsedInDeserialize.Take(listFormat.ElementLength).ToArray().ToInt32(); if (genericArg.IsAssignableFrom(typeof(Byte))) { list.Add(Convert.ToByte(subPrimitiveInstance)); } else if (genericArg.IsAssignableFrom(typeof(int))) { list.Add(subPrimitiveInstance); } else { throw new ArgumentException("Now Only support primitive types of 'byte' and 'int'"); } this.processingRawDataUsedInDeserialize = this.processingRawDataUsedInDeserialize.Skip(listFormat.ElementLength).ToArray(); } else { var subElementInstance = Activator.CreateInstance(genericArg); list.Add(subElementInstance); this.ConvertToObject(subElementInstance, this.processingRawDataUsedInDeserialize); } // no data need to be processing, break it since we might set a big listCount earlier for cascade structure. // or you'll see a big List if (this.processingRawDataUsedInDeserialize.Length == 0) { break; } } } propertyInfo.SetValue(elementInstance, list, null); } } } } /// /// parse the expression and return the operator value. /// /// /// protected virtual int ParseLengthOrCountLinkExpression(string expression) { if (expression != null && (expression.Contains('+') || expression.Contains('-'))) { var pureOperatorAndNumber = expression.Trim().Replace(" ", ""); if (pureOperatorAndNumber.Contains('+')) { return int.Parse(pureOperatorAndNumber.Substring(1)); } else if (pureOperatorAndNumber.Contains('-')) { return -int.Parse(pureOperatorAndNumber.Substring(1)); } } return 0; } /// /// Set the value to a property in a specific object based on the encoding type associated with the property. /// /// /// /// /// private void FillPropertyData(EncodingType encodingType, PropertyInfo propertyInfo, object elementInstance, byte[] data) { switch (encodingType) { case EncodingType.ASCII: case EncodingType.ASCII_PadLeftWithZero: propertyInfo.SetValue(elementInstance, Encoding.ASCII.GetString(data), null); break; case EncodingType.ASCIIInt: propertyInfo.SetValue(elementInstance, int.Parse(Encoding.ASCII.GetString(data)), null); break; case EncodingType.ASCIILong: propertyInfo.SetValue(elementInstance, long.Parse(Encoding.ASCII.GetString(data)), null); break; case EncodingType.BCD: var tempBytes = data; var newValue = tempBytes.Length == 1 ? tempBytes[0].GetBCD() : tempBytes.GetBCD(); if (propertyInfo.PropertyType.FullName == "System.Byte") { propertyInfo.SetValue(elementInstance, (byte)newValue, null); } else { propertyInfo.SetValue(elementInstance, newValue, null); } break; case EncodingType.BcdString: tempBytes = data; //if (propertyInfo.PropertyType.BaseType.FullName == "System.Enum") { // if (!Enum.IsDefined(propertyInfo.PropertyType, tempBytes.GetBCD())) // throw new ArgumentException("The given value is not a valid " + propertyInfo.PropertyType + " element."); //} var fieldValue = tempBytes.Length == 1 ? tempBytes[0].GetBCD().ToString() : tempBytes.GetBCDString(); if (propertyInfo.PropertyType.IsAssignableFrom(typeof(long))) { propertyInfo.SetValue(elementInstance, long.Parse(fieldValue) , null); } else if (propertyInfo.PropertyType.IsAssignableFrom(typeof(DateTime))) { propertyInfo.SetValue(elementInstance, DateTime.ParseExact(fieldValue, "yyyyMMddHHmmss", System.Globalization.CultureInfo.InvariantCulture)); } else if (propertyInfo.PropertyType.IsAssignableFrom(typeof(decimal))) { propertyInfo.SetValue(elementInstance, decimal.Parse(fieldValue) , null); } else { propertyInfo.SetValue(elementInstance, fieldValue , null); } break; case EncodingType.BIN: if (propertyInfo.PropertyType.IsAssignableFrom(typeof(Enum))) { var o = Enum.ToObject(propertyInfo.PropertyType, data.ToInt32()); propertyInfo.SetValue(elementInstance, o, null); } else if (propertyInfo.PropertyType == typeof(Byte)) { try { propertyInfo.SetValue(elementInstance, (byte)(data.ToInt32()), null); } catch (Exception e) { } } // in case the Property is a Nullable enum type. else if (propertyInfo.PropertyType.IsGenericType && propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) { if (propertyInfo.PropertyType.GetGenericArguments()[0].IsEnum) { var o = Enum.ToObject(propertyInfo.PropertyType.GetGenericArguments()[0], data.ToInt32()); propertyInfo.SetValue(elementInstance, o, null); } else { var value = Convert.ChangeType(data.ToInt32(), propertyInfo.PropertyType); propertyInfo.SetValue(elementInstance, value, null); } } else if (propertyInfo.PropertyType == typeof(ushort)) { try { propertyInfo.SetValue(elementInstance, Convert.ToUInt16(data.ToInt32())); } catch (Exception ex) { throw; } } else { propertyInfo.SetValue(elementInstance, data.ToInt32(), null); } break; case EncodingType.HexString: propertyInfo.SetValue(elementInstance, ByteArrayToString(data)); break; case EncodingType.ReverseHexString: propertyInfo.SetValue(elementInstance, ByteArrayToString(data.Reverse().ToArray())); break; } } /// /// /// /// /// /// if length is avarible bytes, then leave null, the serialized bytes will auto fits the actual value. /// /// private byte[] SerializePropertyData(EncodingType encodingType, object propertyValue, int? propertyValueFixedLength, object elementInstance) { string enumTypeConvertedValue; switch (encodingType) { case EncodingType.ASCII: //if (propertyValue is Enum) //{ // var temp = ((int)propertyValue).ToString(); // enumTypeConvertedValue = temp.ToString().PadLeft(propertyValueFixedLength, '0'); //} //else //{ enumTypeConvertedValue = propertyValue.ToString(); //} if (propertyValueFixedLength.HasValue && enumTypeConvertedValue.Length > propertyValueFixedLength.Value) throw new ArgumentOutOfRangeException("ASCII encoded property with value: " + enumTypeConvertedValue + " exceed its declared Fixedlength(" + propertyValueFixedLength + "), try shorten the value"); if (propertyValueFixedLength.HasValue) return Encoding.ASCII.GetBytes(enumTypeConvertedValue.PadRight(propertyValueFixedLength.Value, ' ')); else return Encoding.ASCII.GetBytes(enumTypeConvertedValue); break; case EncodingType.ASCII_PadLeftWithZero: enumTypeConvertedValue = propertyValue.ToString(); if (propertyValueFixedLength.HasValue && enumTypeConvertedValue.Length > propertyValueFixedLength.Value) throw new ArgumentOutOfRangeException("ASCII_PadLeftWithZero encoded property with value: " + enumTypeConvertedValue + " exceed its declared Fixedlength(" + propertyValueFixedLength + "), try shorten the value"); if (propertyValueFixedLength.HasValue) return Encoding.ASCII.GetBytes(enumTypeConvertedValue.PadLeft(propertyValueFixedLength.Value, '0')); else return Encoding.ASCII.GetBytes(enumTypeConvertedValue); break; case EncodingType.ASCIIInt: if (propertyValue is Enum) { var temp = ((int)propertyValue).ToString(); enumTypeConvertedValue = temp.ToString().PadLeft(propertyValueFixedLength.Value, '0'); } else { enumTypeConvertedValue = propertyValue.ToString().PadLeft(propertyValueFixedLength.Value, '0'); ; } return Encoding.ASCII.GetBytes(enumTypeConvertedValue); break; case EncodingType.ASCIILong: if (propertyValue is Enum) { var temp = ((long)propertyValue).ToString(); enumTypeConvertedValue = temp.ToString().PadLeft(propertyValueFixedLength.Value, '0'); } else { enumTypeConvertedValue = propertyValue.ToString().PadLeft(propertyValueFixedLength.Value, '0'); ; } return Encoding.ASCII.GetBytes(enumTypeConvertedValue); break; case EncodingType.BCD: if (propertyValue is Enum) { enumTypeConvertedValue = ((int)propertyValue).ToString(); } else { enumTypeConvertedValue = propertyValue.ToString(); } return long.Parse(enumTypeConvertedValue).GetBCDBytes(propertyValueFixedLength.Value); break; case EncodingType.BcdString: if (propertyValue is Enum) { enumTypeConvertedValue = ((int)propertyValue).ToString(); } else { enumTypeConvertedValue = propertyValue.ToString(); } return System.Numerics.BigInteger.Parse(enumTypeConvertedValue).GetBCDBytes(propertyValueFixedLength.Value); break; case EncodingType.BIN: if (propertyValue is Enum) { enumTypeConvertedValue = ((int)propertyValue).ToString(); } else { enumTypeConvertedValue = propertyValue.ToString(); } if (propertyValueFixedLength.HasValue) { if (long.Parse(enumTypeConvertedValue) > int.MaxValue) { return long.Parse(enumTypeConvertedValue).GetBinBytes(propertyValueFixedLength.Value); } else { return (int.Parse(enumTypeConvertedValue)).GetBinBytes(propertyValueFixedLength.Value); } } else { logger.Info($"Parsing value: {enumTypeConvertedValue}"); return (int.Parse(enumTypeConvertedValue)).GetBinBytes(1); } break; case EncodingType.HexString: return StringToByteArray(propertyValue.ToString()); break; } return null; } public static byte[] StringToByteArray(string hex) { int numberChars = hex.Length; byte[] bytes = new byte[numberChars / 2]; for (int i = 0; i < numberChars; i += 2) bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); return bytes; } public static string ByteArrayToString(byte[] arr) { return BitConverter.ToString(arr).Replace("-", ""); } static bool IsNullable(T obj) { if (obj == null) return true; // obvious Type type = typeof(T); if (!type.IsValueType) return true; // ref-type if (Nullable.GetUnderlyingType(type) != null) return true; // Nullable return false; // value-type } private byte[] ConvertToBytes(object elementInstance) { // Only processing MessageAttribute marked properties, and order by its Index in DESC, like 10, 9, 8, 7.... -1, -2...-10. var targetPropertyList = elementInstance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .Where(p => p.GetCustomAttributes(typeof(AttributeBase), true).Length > 0) .OrderByDescending(info => ((AttributeBase)(info.GetCustomAttributes(typeof(AttributeBase), true)[0])).Index); foreach (var propertyInfo in targetPropertyList) { // normal Format Property if (propertyInfo.GetCustomAttributes(typeof(EnumerableFormatAttribute), true).Length == 0 && propertyInfo.GetCustomAttributes(typeof(FormatAttribute), true).Length > 0) { #region validate the Range Attibute marked property, if existed and failed to pass, throw exception // Validate the range, once failed, throw a exception var propertyRangeAttributes = propertyInfo.GetCustomAttributes(typeof(RangeAttribute), true); if (propertyRangeAttributes.Length > 0) { var rangeAttribute = (RangeAttribute)propertyRangeAttributes[0]; if (rangeAttribute != null) { if (!rangeAttribute.IsValid(propertyInfo.GetValue(elementInstance, null))) { throw new ArgumentException(rangeAttribute.ErrorMessage); } } } #endregion var format = (FormatAttribute)propertyInfo.GetCustomAttributes(typeof(FormatAttribute), true)[0]; if (format.LengthOrCountLinkExpression != null && format.LengthOrCountLinkExpression.Contains("%OnSerializingBytesCount") ) { var lengthValue = this.processingRawDataUsedInSerialize.Length; // if this property's value is default with null (most likely the nullable int), then will not set any value on it, the serializing // result will omit this property. if (lengthValue == 0 && propertyInfo.GetValue(elementInstance, null) == null) { } else propertyInfo.SetValue(elementInstance, this.processingRawDataUsedInSerialize.Length, null); } else if (!string.IsNullOrEmpty(format.LengthOrCountLink)) { var lengthLinkProperty = targetPropertyList.FirstOrDefault(t => t.Name == format.LengthOrCountLink); if (lengthLinkProperty != null) { propertyInfo.SetValue(elementInstance, propertyInfo.GetValue(elementInstance, null).ToString().Length, null); } } var propertyValue = propertyInfo.GetValue(elementInstance, null); // if this property value is null, then indicate it's a optional field, then skip to parsing it. if (propertyValue == null) { var safeEvent = this.FieldSerializing; safeEvent?.Invoke(this, new ParsingEventArg() { From = propertyInfo.Name, To = null }); continue; } var safe = this.FieldSerializing; safe?.Invoke(this, new ParsingEventArg() { From = propertyInfo.Name, To = null }); byte[] currentPropertyBytes = this.SerializePropertyData(format.EncodingType, propertyValue, format.FixedLengthOrCount > 0 ? format.FixedLengthOrCount : default(int?), elementInstance); safe = this.FieldSerialized; safe?.Invoke(this, new ParsingEventArg() { From = propertyInfo.Name, To = currentPropertyBytes }); this.processingRawDataUsedInSerialize = this.processingRawDataUsedInSerialize.AppendToHeader(currentPropertyBytes); if (format.Index >= 0) this.positiveIndexAccu += currentPropertyBytes.Length; // try fill the LengthLink field. //if (!string.IsNullOrEmpty(format.LengthOrCountLink)) //{ // var lengthLinkProperty = targetPropertyList.FirstOrDefault(t => t.Name == format.LengthOrCountLink); // if (lengthLinkProperty != null // && string.IsNullOrEmpty(format.LengthOrCountLinkExpression) // // if it's not a LengthOrCountLinkExpression field, then the linking field is directly reflected the length, so it must euqal 0 // // if not, means the users code set the length explicitly, accept this behavior to NOT overwirte it. // && int.Parse(lengthLinkProperty.GetValue(elementInstance, null).ToString()) == 0) // { // lengthLinkProperty.SetValue(elementInstance, currentPropertyBytes.Length, null); // } // else if (false && !string.IsNullOrEmpty(format.LengthOrCountLinkExpression)) // { // var pair = format.LengthOrCountLinkExpression.Split(';'); // bool found = false; // foreach (string p in pair) // { // var mappings = p.Split(':'); // if (currentPropertyBytes.Length == int.Parse(mappings[1])) // { // //lengthLinkProperty.SetValue(elementInstance, int.Parse(mappings[0], null)); // lengthLinkProperty.SetValue(elementInstance, int.Parse(mappings[0], null), null); // found = true; // break; // } // } // if (!found) // { throw new ArgumentException("Serializing LengthOrCountLinkExpression: " + format.LengthOrCountLinkExpression + " didn't found match values in link field: " + format.LengthOrCountLink); } // } //} } // the Enumerable Property else if (propertyInfo.GetCustomAttributes(typeof(EnumerableFormatAttribute), true).Length > 0) { var enumerableFormat = (EnumerableFormatAttribute)propertyInfo.GetCustomAttributes(typeof(EnumerableFormatAttribute), true)[0]; // for now, we only support List if (propertyInfo.PropertyType.IsAssignableFrom(typeof(IList))) { throw new ArgumentException("For now, we only support Enumerable of List"); } var list = (IList)propertyInfo.GetValue(elementInstance, null); if (list != null) { if (enumerableFormat.FixedLengthOrCount > 0) { if (enumerableFormat.FixedLengthOrCount != list.Count) { throw new ArgumentException("The current count(" + list.Count + ") of EnumerableFormat property: " + propertyInfo.Name + " must match its declared FixedLengthOrCount(" + enumerableFormat.FixedLengthOrCount + ")"); } } if (list.GetType().GetGenericArguments()[0].IsPrimitive) { var safe = this.FieldSerializing; if (safe != null) { safe(this, new ParsingEventArg() { From = propertyInfo.Name, To = null }); } byte[] primitiveSerilaizedBytes = new byte[0]; for (int i = list.Count - 1; i >= 0; i--) { primitiveSerilaizedBytes = primitiveSerilaizedBytes.AppendToHeader( this.SerializePropertyData( enumerableFormat.EncodingType, list[i], 1, elementInstance) ); } this.processingRawDataUsedInSerialize = this.processingRawDataUsedInSerialize.AppendToHeader(primitiveSerilaizedBytes); if (enumerableFormat.Index >= 0) this.positiveIndexAccu += primitiveSerilaizedBytes.Length; safe = this.FieldSerialized; if (safe != null) { safe(this, new ParsingEventArg() { From = propertyInfo.Name, To = primitiveSerilaizedBytes }); } } else { // reverse for (int i = list.Count - 1; i >= 0; i--) { this.processingRawDataUsedInSerialize.AppendToHeader(this.ConvertToBytes(list[i])); } } if (!string.IsNullOrEmpty(enumerableFormat.LengthOrCountLink) && enumerableFormat.LengthOrCountLink.ToLower() != "%cascade") { var countLinkProperty = targetPropertyList.FirstOrDefault(t => t.Name == enumerableFormat.LengthOrCountLink); if (countLinkProperty != null) { try { countLinkProperty.SetValue(elementInstance, list.Count, null); } catch (Exception ex) { } } } else { if (!string.IsNullOrEmpty(enumerableFormat.LengthOrCountLink) && enumerableFormat.LengthOrCountLink.ToLower() != "%cascade") { targetPropertyList.First(t => t.Name == enumerableFormat.LengthOrCountLink). SetValue(elementInstance, 0, null); } } } } } return this.processingRawDataUsedInSerialize; } // make sure the Indexes property of one element including the sub elements // of its list structures are sequential and unique protected void CheckAttributeIndexes(object elementInstance) { if (elementInstance == null) { return; } // Only processing WayneAttribute marked properties, and order by its Index in DESC var targetPropertyList = elementInstance.GetType() .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .Where(p => p.GetCustomAttributes(typeof(AttributeBase), true).Length > 0) .OrderByDescending( info => ((AttributeBase)(info.GetCustomAttributes(typeof(AttributeBase), true)[0])) .Index); // make sure no duplicated Index defined. targetPropertyList.GroupBy( p => ((AttributeBase)(p.GetCustomAttributes(typeof(AttributeBase), true)[0])).Index) .ToList() .ForEach(g => { if (g.Count() > 1) { throw new ArgumentException( string.Format("Duplicated Index: {0} defined, make sure the Index value unique", g.Key)); } }); // make sure the Indexes are contiguously sequential and count from 0 if (targetPropertyList.Max( p => ((AttributeBase)(p.GetCustomAttributes(typeof(AttributeBase), true)[0])).Index) != (targetPropertyList.Count() - 1)) { throw new ArgumentException(string.Format("Index must be in sequential and count from 0")); } // recursively check the sub element type of ListPropertyFormatAttribute foreach (var propertyInfo in targetPropertyList) { // the IList Property if (propertyInfo.GetCustomAttributes(typeof(EnumerableFormatAttribute), true).Length != 0) { var subElementInstance = Activator.CreateInstance(propertyInfo.PropertyType.GetGenericArguments()[0]); CheckAttributeIndexes(subElementInstance); } } } } }