MY DECISION:
Attribute Definition:
[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] internal sealed class SymitarInquiryDataFormatAttribute : Attribute { private readonly string _name;
Data class:
[Serializable] public class SymitarAccount { public int PositionalIndex; public bool IsClosed{get { return CloseDate.HasValue; }} [SymitarInquiryDataFormatAttribute("ID")] public int Id; [SymitarInquiryDataFormatAttribute("CLOSEDATE")] public DateTime? CloseDate; [SymitarInquiryDataFormatAttribute("DIVTYPE")] public int DivType; [SymitarInquiryDataFormatAttribute("BALANCE")] public decimal Balance; [SymitarInquiryDataFormatAttribute("AVAILABLEBALANCE")] public decimal AvailableBalance; }
Extensions:
public static class ExtensionSymitar { public static List<string> ValueList(this string source, string fieldType) { var list = source.Split('~').ToList(); return list.Where(a => a.StartsWith(fieldType)).ToList(); } public static string KeyValuePairs(this string source, string fieldType) { return source.ValueList(fieldType).Aggregate(string.Empty, (current, j) => string.Format("{0}~{1}", current, j)); } public static bool IsMultiRecord(this string source, string fieldType) { return source.ValueList(fieldType) .Select(q => new Regex(Regex.Escape(q.Split('=').First())).Matches(source).Count > 1).First(); } public static int ParseInt(this string val, string keyName) { int newValue; if (!int.TryParse(val, out newValue)) throw new Exception("Could not parse " + keyName + " as an integer!"); return newValue; } public static decimal ParseMoney(this string val, string keyName) { decimal newValue; if (!decimal.TryParse(val, out newValue)) throw new Exception("Could not parse " + keyName + " as a money amount!"); return newValue; } public static DateTime? ParseDate(this string val, string keyName) { if (val.Equals("00000000")) return null; var year = val.Substring(0, 4).ToInt(); var mon = val.Substring(4, 2).ToInt(); var day = val.Substring(6, 2).ToInt(); if (year <= 1800 || year >= 2200 || mon < 1 || mon > 12 || day < 1 || day > 31) throw new Exception("Could not parse " + keyName + " as a date!"); return new DateTime(year, mon, day); } }
deserializer:
public class SymitarInquiryDeserializer { /// <summary> /// Deserializes a string of J field key value pairs /// </summary> /// <param name="str">The request or response string</param> /// <param name="source">Optional: Use this if you are adding data to the source object</param> /// <param name="fieldName">Optional: Use this if you are only populating a single property and know what it is</param> /// <typeparam name="T">The target class type to populate</typeparam> /// <returns>New T Object or optional Source Object</returns> public static T DeserializeFieldJ<T>(string str, T source = null, string fieldName = null) where T : class, new() { var result = source ?? new T(); const string pattern = @"(?:~J(\w+=\d+))*"; var match = Regex.Match(str, pattern); // Get fields of type T var fields = typeof(T).GetFields(BindingFlags.Public | BindingFlags.Instance).ToList(); if (fieldName != null && fieldName.StartsWith("J")) fieldName = fieldName.Replace("J", ""); if (!fieldName.IsNullOrEmpty()) { var field = fields.FirstOrDefault(a => a.Name.Equals(fieldName, StringComparison.CurrentCultureIgnoreCase)); var stringValue = GetValue(field, match); if (!stringValue.IsNullOrEmpty()) SetProperty(field, stringValue, result); } else { foreach (var field in fields) { var stringValue = GetValue(field, match); if(!stringValue.IsNullOrEmpty()) SetProperty(field, stringValue, result); } } return result; } private static string GetValue(FieldInfo field, Match match) { // Get out custom attribute of this field (might return null) var attr = field.GetCustomAttribute(typeof(SymitarInquiryDataFormatAttribute)) as SymitarInquiryDataFormatAttribute; if (attr == null) return null; // Find regex capture that starts with attributed name (might return null) var capture = match.Groups[1] .Captures .Cast<Capture>() .FirstOrDefault(c => c.Value.StartsWith(attr.Name, StringComparison.CurrentCultureIgnoreCase)); return capture == null ? null : capture.Value.Split('=').Last(); } private static void SetProperty<T>(FieldInfo field, string stringValue, T result) { // Convert string to the proper type (like int) if (field.FieldType.FullName.Contains("Int32")) field.SetValue(result, stringValue.ParseInt(field.Name)); else if (field.FieldType.FullName.Contains("Decimal")) field.SetValue(result, stringValue.ParseMoney(field.Name)); else if (field.FieldType.FullName.Contains("DateTime")) field.SetValue(result, stringValue.ParseDate(field.Name)); else { var value = Convert.ChangeType(stringValue, field.FieldType); field.SetValue(result, value); } } }
Finally, in my repository:
public List<SymitarAccount> GetAccounts(string accountId) { var accountList = new List<SymitarAccount>(); // build request, get response, parse it... var request = "IQ~1~A20424~BAUTOPAY~D101~F7777~HSHARE=0~JID=ALL"; var response = UnitOfWork.SendMessage(request); ParseResponse(response, ref accountList); foreach (var account in accountList.Where(a => a.IsClosed == false)) { request = "IQ~1~A20424~BAUTOPAY~D101~F7777~HSHARE#" + account.Id.ToString("0000") + "~JCLOSEDATE~JSHARECODE~JDIVTYPE~JBALANCE~JAVAILABLEBALANCE"; response = UnitOfWork.SendMessage(request); ParseResponse(response, ref accountList, account.Id); } return accountList; } private void ParseResponse(string response, ref List<SymitarAccount> accountList, int? id = null) { var index = 0; var list = response.ValueList(fieldType: "J"); var jString = response.KeyValuePairs(fieldType: "J"); var isMultiRecord = response.IsMultiRecord(fieldType: "J"); SymitarAccount account; if (isMultiRecord && !id.HasValue) foreach (var q in list.Where(a => a.StartsWith("J"))) { // Add object if we don't yet have it in the collection... if (accountList.Count <= index) accountList.Add(new SymitarAccount { PositionalIndex = index }); account = accountList.FirstOrDefault(a => a.PositionalIndex == index); SymitarInquiryDeserializer.DeserializeFieldJ("~" + q, account, q.Split('=').First()); index++; } else if(id.HasValue) { account = accountList.FirstOrDefault(a => a.Id == id.Value); SymitarInquiryDeserializer.DeserializeFieldJ(jString, account); } }
The difference between the two calls to ParseResponse is that in the first case I ask you to return several records (only one data property!), And in the second case I ask for additional data properties for one record to be sent back.