3
\$\begingroup\$

This code is part of a larger mapping library I'm working on to address some business concerns of transforming data. I was inspired by mapstruct in Java, but opted for users to annotate / add attributes to a poco to control the mapping operation.

Because reflection can be an expensive operation, I opted for scanning all loaded assemblies at run time (and filtering on criteria) and caching the data in memory. Below I am fetching metadata for types by going to the cache and accessing both the common property context and xml property context to check for certain conditions.

Some code has been omitted to keep this focused, but the rest of the repo is here: https://github.com/Igneous01/Integration

Sample of how code is exercised

public class BooleanConverter : IXmlPropertyConverter<bool>
{
    public bool ConvertToSourceType(string source)
    {
        if ("y".Equals(source.ToLower()))
            return true;
        else
            return false;
    }

    public string ConvertToDestinationType(bool source)
    {
        if (source)
            return "Y";
        else
            return "N";
    }
}

public class DateTimeConverter : IXmlPropertyConverter<DateTime>
{
    private const string DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";

    public DateTime ConvertToSourceType(string source)
    {
        return DateTime.ParseExact(source, DATE_TIME_FORMAT, System.Globalization.CultureInfo.InvariantCulture);
    }

    public string ConvertToDestinationType(DateTime source)
    {
        return source.ToString(DATE_TIME_FORMAT);
    }
}

[IncludeInScan]
public class UnMarked
{
    public int UnmarkedFieldA { get; set; }
    public double UnmarkedFieldB { get; set; }
}

[XmlMapper(ParentNodeName = "Applicant", MappingOperation = XmlMappingOperation.NODE)]
public class Applicant
{
    [Name("name")]
    public string FirstName { get; set; }

    [Name("surname")]
    public string LastName { get; set; }
}

[XmlMapper(ParentNodeName = "ApplicationNode", 
    MappingOperation = XmlMappingOperation.NODE,
    IgnoreNulls = true)]
public class Application
{
    public int ApplicationID { get; set; }

    [XmlPropertyConverter(typeof(DateTimeConverter))]
    [Name("date")]
    public DateTime CreateDate { get; set; }

    public string Name { get; set; }

    public object AnotherNull { get; set; }
}

[XmlMapper(ParentNodeName = "Loan", 
    MappingOperation = XmlMappingOperation.NODE, 
    IgnoreNulls = false)]
public class Loan
{
    [Ignore]
    public string ProductCode { get; set; }

    [Name("name")]
    public string Name { get; set; }

    [Name("amount")]
    public int Amount { get; set; }

    [Name("joint")]
    [XmlPropertyConverter(typeof(BooleanConverter))]
    public bool IsJointAccount { get; set; }

    [Name("applicant")]
    public Applicant Applicant { get; set; }

    public Application Application { get; set; }

    [Name("CustomList")]
    [XmlList(NodeName = "ListingItem")]
    public List<string> Listing { get; set; }

    public object Null { get; set; }
}

[XmlMapper(ParentNodeName = "CustomLoan", 
    MappingOperation = XmlMappingOperation.ATTRIBUTE, 
    IgnoreNulls = true,
    Formatting = System.Xml.Linq.SaveOptions.None)]
public class CustomLoan : Loan
{
    public string CustomFieldA { get; set; }
    public string CustomFieldB { get; set; }

    [XmlFlattenHierarchy]
    public UnMarked FlattenThisProperty { get; set; }

    public Dictionary<string, UnMarked> Dictionary { get; set; }
    public LinkedList<Applicant> JointApplicants { get; set; }
}

static void Main(string[] args)
    {
        List<CustomLoan> loans = new List<CustomLoan>();
        for (int i = 0; i < 1000; i++)
        {
            loans.Add(new CustomLoan()
                {
                    Name = "Revolving Loan",
                    Amount = 25000,
                    IsJointAccount = true,
                    ProductCode = "RLP",
                    Applicant = new Applicant()
                    {
                        FirstName = "George",
                        LastName = "Bush"
                    },
                    Application = new Application()
                    {
                        ApplicationID = 1,
                        CreateDate = new DateTime(2018, 5, 22),
                        Name = "New Application"
                    },
                    Listing = new List<string>() { "string", "another", "text" },
                    CustomFieldA = "Custom Data For Client Here",
                    CustomFieldB = "Custom Other Data For Client Here",
                    Dictionary = new Dictionary<string, UnMarked>()
                        {
                            { "KeyA", new UnMarked(){ UnmarkedFieldA = 20, UnmarkedFieldB = 3.14564 } },
                            { "KeyB", new UnMarked(){ UnmarkedFieldA = 77, UnmarkedFieldB = 16.981357 } },
                            { "KeyC", new UnMarked(){ UnmarkedFieldA = 86486, UnmarkedFieldB = -1.1384 } }
                        },
                    JointApplicants = new LinkedList<Applicant>(new List<Applicant>
                        {
                            new Applicant() { FirstName = "Jemma", LastName = "Busher" },
                            new Applicant() { FirstName = "Harrison", LastName = "Bushmaster" }
                        })
            });   
        }


        var xmlMapper = new XmlMapper<CustomLoan>();
        string values = "";

        Stopwatch watch = Stopwatch.StartNew();

        foreach (CustomLoan loan in loans)
            values = xmlMapper.MapToXmlString(loan);    

        watch.Stop();

        Console.WriteLine(watch.ElapsedMilliseconds);
        Console.WriteLine(values);

        XmlDocument xml = new XmlDocument();
        xml.LoadXml(values);

        CustomLoan mappedBackObject = xmlMapper.MapToObject(xml);

        //Console.WriteLine("Serialization");
        //Console.WriteLine(ToXML(loans[0]));

        Console.ReadKey();
        return;
    }

Output

<CustomLoan CustomFieldA="Custom Data For Client Here" CustomFieldB="Custom Other Data For Client Here" name="Revolving Loan" amount="25000" joint="Y">
  <Dictionary>
    <KeyA UnmarkedFieldA="20" UnmarkedFieldB="3.14564" />
    <KeyB UnmarkedFieldA="77" UnmarkedFieldB="16.981357" />
    <KeyC UnmarkedFieldA="86486" UnmarkedFieldB="-1.1384" />
  </Dictionary>
  <JointApplicants>
    <Applicant>
      <name>Jemma</name>
      <surname>Busher</surname>
    </Applicant>
    <Applicant>
      <name>Harrison</name>
      <surname>Bushmaster</surname>
    </Applicant>
  </JointApplicants>
  <applicant>
    <name>George</name>
    <surname>Bush</surname>
  </applicant>
  <Application>
    <ApplicationID>1</ApplicationID>
    <date>2018-05-22 00:00:00</date>
    <Name>New Application</Name>
  </Application>
  <CustomList>
    <ListingItem>string</ListingItem>
    <ListingItem>another</ListingItem>
    <ListingItem>text</ListingItem>
  </CustomList>
</CustomLoan>

I did a basic performance check and was averaging ~140ms to map 1000 objects of this complexity.

I am having issues simplifying some of the conditions in the ToObject and ToXml methods. I think I can wrap some of these into private methods to simplify things, but I find the logic difficult to follow at times.

You may have noticed that I am not doing any error checking (or checking that the attributes are compatible with the types in question) - I am validating all of this when constructing the MetaDataCache, where I throw exceptions on startup if attributes are improperly configured. However there are probably some spots I should be error checking but I missed.

Any tips on how to simplify the mapping code would be much appreciated.

public class XmlMapper<T> where T : new()
{
    private const string XPATH_ROOT_NODE_EXPRESSION = "(/*)";
    private Type type = typeof(T);
    // this is a hacky way of asking the XmlMapperAttribute what are it's default values in the case that the class does not have this attribute on it
    private XmlMapperAttribute xmlMapperAttribute = new XmlMapperAttribute();

    public XmlMapper()
    {
        if (MetaDataCache.Contains<T>())
        {
            ClassMetaData metaData = MetaDataCache.Get<T>();
            if (metaData.ClassAttributeContext.ContainsAttribute<XmlMapperAttribute>())
                xmlMapperAttribute = metaData.ClassAttributeContext.GetAttribute<XmlMapperAttribute>();
        }         
    }

    public T MapToObject(XmlDocument xml)
    {
        T instance = new T();
        instance = (T)ToObject(instance, xml.SelectSingleNode(XPATH_ROOT_NODE_EXPRESSION));
        return instance;
    }

    public string MapToXmlString(T instance)
    {
        return ToXmlWrapper(type, instance).ToString(xmlMapperAttribute.Formatting);
    }

    public XmlDocument MapToXmlDocument(T instance)
    {
        return ToXmlWrapper(type, instance).ToXmlDocument();
    }

    private object ToObject(object instance, XmlNode xmlNode, string nodeName = null)
    {
        System.Type type = instance.GetType();
        ClassMetaData classMetaData = MetaDataCache.Get(type);
        XmlMappingOperation xmlMappingOperation = xmlMapperAttribute.MappingOperation;

        if (classMetaData.ClassAttributeContext.ContainsAttribute<XmlMapperAttribute>())
        {
            XmlMapperAttribute xmlMapper = classMetaData.ClassAttributeContext.GetAttribute<XmlMapperAttribute>();
            xmlMappingOperation = xmlMapper.MappingOperation;
            if (nodeName == null)
            {
                nodeName = xmlMapper.ParentNodeName;
                xmlNode = xmlNode.SelectSingleNode($"//{nodeName}");
            }
        }

        foreach (PropertyInfo property in classMetaData.Properties)
        {
            string propertyName = property.Name;
            System.Type propertyType = property.PropertyType;

            if (classMetaData.HasPropertyAttributeContext<PropertyAttributeContext>(property))
            {
                PropertyAttributeContext propertyAttributeContext = classMetaData.GetAttributeContextForProperty<PropertyAttributeContext>(property);

                if (propertyAttributeContext.HasIgnoreAttribute)
                    continue;

                propertyName = propertyAttributeContext.HasNameAttribute ? propertyAttributeContext.NameAttribute.Name : propertyName;
            }

            object propertyValue;

            if (xmlMappingOperation.Equals(XmlMappingOperation.NODE))
            {
                XmlNode propertyNode = xmlNode.SelectSingleNode($"//{nodeName}/{propertyName}");
                propertyValue = propertyNode?.InnerText;
            }
            else // ATTRIBUTE
            {
                XmlNode propertyNode = xmlNode.SelectSingleNode($"//{nodeName}");
                propertyValue = propertyNode.Attributes[propertyName]?.Value;
            }

            if (classMetaData.HasPropertyAttributeContext<XmlPropertyAttributeContext>(property))
            {
                XmlPropertyAttributeContext xmlPropertyAttributeContext = classMetaData.GetAttributeContextForProperty<XmlPropertyAttributeContext>(property);

                if (xmlPropertyAttributeContext.HasXmlListAttribute)
                {
                    XmlListAttribute xmlList = xmlPropertyAttributeContext.XmlListAttribute;
                    XmlNodeList results = xmlNode.SelectNodes($"//{nodeName}/{propertyName}/{xmlList.NodeName}");
                    if (results.Count > 0)
                    {
                        object childInstance = CollectionXmlNodeListToObject(results, propertyType);
                        property.SetValue(instance, childInstance);
                    }                    
                    continue;
                }
                else if (xmlPropertyAttributeContext.HasXmlDictionaryAttribute)
                {
                    XmlDictionaryAttribute xmlList = xmlPropertyAttributeContext.XmlDictionaryAttribute;
                    XmlNodeList results = xmlNode.SelectNodes($"//{nodeName}/{propertyName}/*");
                    if (results.Count > 0)
                    {
                        object childInstance = DictionaryXmlNodeListToObject(results, propertyType);
                        property.SetValue(instance, childInstance);
                    }
                    continue;
                }
                else if (xmlPropertyAttributeContext.HasXmlFlattenHierarchyAttribute)
                {
                    object childInstance = Activator.CreateInstance(propertyType);
                    XmlNode results = xmlNode.SelectSingleNode($"//{nodeName}/{propertyName}");
                    if (results != null)
                    {
                        childInstance = ToObject(childInstance, results, propertyName);
                        property.SetValue(instance, childInstance);
                    }
                    continue;
                }
                else if (xmlPropertyAttributeContext.HasXmlPropertyConverterAttribute && propertyValue != null)
                {
                    XmlPropertyConverterAttribute converter = xmlPropertyAttributeContext.XmlPropertyConverterAttribute;
                    try
                    {
                        propertyValue = converter.ConvertToSourceType(propertyValue);
                    }
                    catch (Exception ex)
                    {
                        throw new XmlPropertyConverterException("XmlPropertyConverter threw an exception", ex);
                    }
                }
            }
            else
            {
                if (propertyType.IsDictionary())
                {
                    XmlNodeList results = xmlNode.SelectNodes($"//{nodeName}/{propertyName}/*");
                    if (results.Count > 0)
                    {
                        object childInstance = DictionaryXmlNodeListToObject(results, propertyType);
                        property.SetValue(instance, childInstance);
                    }                   
                    continue;
                }
                else if (propertyType.IsCollection())
                {
                    string listItemNodeName = propertyType.GenericTypeArguments[0].Name;
                    XmlNodeList results = xmlNode.SelectNodes($"//{nodeName}/{propertyName}/{listItemNodeName}");
                    if (results.Count > 0)
                    {
                        object childInstance = CollectionXmlNodeListToObject(results, propertyType);
                        property.SetValue(instance, childInstance);
                    }                 
                    continue;
                }            
                if (propertyType.IsClass && MetaDataCache.Contains(propertyType))
                {
                    // TODO: Dont think this will work
                    object childInstance = Activator.CreateInstance(propertyType);
                    XmlNode results = xmlNode.SelectSingleNode($"//{nodeName}/{propertyName}");
                    if (results != null)
                    {
                        childInstance = ToObject(childInstance, results, propertyName);
                        property.SetValue(instance, childInstance);
                    }           
                    continue;
                }
            }

            property.SetValue(instance, UniversalTypeConverter.Convert(propertyValue, propertyType));
        }

        return instance;
    }

    private object CollectionXmlNodeListToObject(XmlNodeList nodeList, Type collectionType)
    {
        object collection = CreateInstanceOfType(collectionType);     
        Type containedType = collectionType.GetTypeInfo().GenericTypeArguments[0];
        Type iCollectionType = typeof(ICollection<>).MakeGenericType(containedType);

        foreach (XmlNode node in nodeList)
        {
            object value = CreateInstanceOfType(containedType);

            if (containedType.IsClass && MetaDataCache.Contains(containedType))
                value = ToObject(value, node, node.Name);
            else
                value = node.InnerText;

            iCollectionType.GetMethod("Add").Invoke(collection, new[] { value });
        }

        return collection;
    }

    private object DictionaryXmlNodeListToObject(XmlNodeList nodeList, System.Type dictionaryType)
    {
        object dictionary = Activator.CreateInstance(dictionaryType);
        Type keyType = dictionaryType.GetTypeInfo().GenericTypeArguments[0];
        Type valueType = dictionaryType.GetTypeInfo().GenericTypeArguments[1];

        foreach (XmlNode node in nodeList)
        {
            object key = node.Name; // will be replaced with dictionary mapper later
            object value = CreateInstanceOfType(valueType);

            if (valueType.IsClass && MetaDataCache.Contains(valueType))
                value = ToObject(value, node, node.Name);
            else
                value = node.InnerText;

            dictionaryType.GetMethod("Add").Invoke(dictionary, new[] { node.Name, value });
        }

        return dictionary;
    }

    private object CreateInstanceOfType(Type type)
    {
        if (!type.HasDefaultConstructor())
            return null;
        else
            return Activator.CreateInstance(type);
    }

    private XDocument ToXmlWrapper(Type type, T instance)
    {         
        XElement rootElement = ToXml(instance, type);
        XDocument xDocument = new XDocument();
        xDocument.Add(rootElement);     

        // will throw exception if fails to construct XmlDocument
        if (xmlMapperAttribute.Validate)
            xDocument.ToXmlDocument();

        return xDocument;
    }

    private XElement ToXml(object instance, Type type, XElement element = null, string nodeName = null)
    {
        ClassMetaData classMetaData = MetaDataCache.Get(type);
        XmlMappingOperation xmlMappingOperation = xmlMapperAttribute.MappingOperation;
        bool ignoreNulls = xmlMapperAttribute.IgnoreNulls;

        if (classMetaData.ClassAttributeContext.ContainsAttribute<XmlMapperAttribute>())
        {
            XmlMapperAttribute xmlMapper = classMetaData.ClassAttributeContext.GetAttribute<XmlMapperAttribute>();
            xmlMappingOperation = xmlMapper.MappingOperation;
            ignoreNulls = xmlMapper.IgnoreNulls;

            if (element == null)
            {
                element = new XElement(xmlMapper.ParentNodeName);
                nodeName = xmlMapper.ParentNodeName;
            }
        }

        //element.Name = nodeName;

        foreach (PropertyInfo property in classMetaData.Properties)
        {
            string propertyName = property.Name;
            object propertyValue = property.GetValue(instance);
            System.Type propertyType = property.PropertyType;        

            if (propertyValue == null && ignoreNulls)
                continue;

            if (classMetaData.HasPropertyAttributeContext<PropertyAttributeContext>(property))
            {
                PropertyAttributeContext propertyAttributeContext = classMetaData.GetAttributeContextForProperty<PropertyAttributeContext>(property);
                if (propertyAttributeContext.HasIgnoreAttribute)
                    continue;

                propertyName = propertyAttributeContext.HasNameAttribute ? propertyAttributeContext.NameAttribute.Name : propertyName;
            }

            if (classMetaData.HasPropertyAttributeContext<XmlPropertyAttributeContext>(property))
            {
                XmlPropertyAttributeContext xmlPropertyAttributeContext = classMetaData.GetAttributeContextForProperty<XmlPropertyAttributeContext>(property);

                if (xmlPropertyAttributeContext.HasXmlPropertyConverterAttribute && propertyValue != null)
                {
                    XmlPropertyConverterAttribute converter = xmlPropertyAttributeContext.XmlPropertyConverterAttribute;
                    try
                    {
                        propertyValue = converter.ConvertToDestinationType(propertyValue);
                        AddToXElement(element, xmlMappingOperation, propertyName, propertyValue);
                        continue;
                    }
                    catch (Exception ex)
                    {
                        throw new XmlPropertyConverterException("XmlPropertyConverter threw an exception", ex);
                    }
                }
                else if (xmlPropertyAttributeContext.HasXmlDictionaryAttribute)
                {
                    XmlDictionaryAttribute xmlDictionary = xmlPropertyAttributeContext.XmlDictionaryAttribute;
                    element.Add(DictionaryToXElement(propertyValue, propertyName));
                    continue;
                }
                else if (xmlPropertyAttributeContext.HasXmlListAttribute)
                {
                    XmlListAttribute xmlList = xmlPropertyAttributeContext.XmlListAttribute;
                    element.Add(CollectionToXElement(propertyValue, propertyName, xmlList.NodeName));
                    continue;
                }                   
                else if (xmlPropertyAttributeContext.HasXmlFlattenHierarchyAttribute)
                {
                    element = ToXml(propertyValue, propertyType, element, propertyName);
                    continue;
                }
            }
            else
            {
                if (propertyType.IsDictionary())
                {
                    element.Add(DictionaryToXElement(propertyValue, propertyName));
                    continue;
                }
                else if (propertyType.IsCollection())
                {
                    element.Add(CollectionToXElement(propertyValue, propertyName, propertyType.GenericTypeArguments[0].Name));
                    continue;
                }              
                else if (propertyType.IsClass && MetaDataCache.Contains(propertyType))
                {
                    XElement propertyElement = new XElement(propertyName);
                    propertyElement = ToXml(propertyValue, propertyType, propertyElement, propertyName);
                    element.Add(propertyElement);
                    continue;
                }
            }

            AddToXElement(element, xmlMappingOperation, propertyName, propertyValue);
        }

        return element;
    }

    private void AddToXElement(XElement element, XmlMappingOperation xmlMappingOperation, string propertyName, object propertyValue)
    {
        if (xmlMappingOperation.Equals(XmlMappingOperation.ATTRIBUTE))
            element.SetAttributeValue(propertyName, propertyValue);
        else if (xmlMappingOperation.Equals(XmlMappingOperation.NODE))
            element.Add(new XElement(propertyName, propertyValue));
    }

    private XElement CollectionToXElement(object collection, string parentNodeName, string childNodeName)
    {
        XElement propertyElement = new XElement(parentNodeName);
        Type containedType = collection.GetType().GenericTypeArguments[0];

        foreach (var item in (ICollection)collection)
        {
            XElement itemElement = new XElement(childNodeName);
            if (containedType.IsClass && MetaDataCache.Contains(containedType))
                itemElement = ToXml(item, item.GetType(), itemElement, childNodeName);
            else
                itemElement.SetValue(item);

            propertyElement.Add(itemElement);
        }

        return propertyElement;
    }

    private XElement DictionaryToXElement(object dictionary, string parentNodeName)
    {
        XElement propertyElement = new XElement(parentNodeName);
        Type keyType = dictionary.GetType().GenericTypeArguments[0];
        Type valueType = dictionary.GetType().GenericTypeArguments[1];

        foreach (DictionaryEntry kvp in (IDictionary)dictionary)
        {
            // TODO - this will call converter that converts Dictionary key to XElement
            XElement itemElement = new XElement(kvp.Key.ToString());
            if (valueType.IsClass && MetaDataCache.Contains(valueType))
                itemElement = ToXml(kvp.Value, kvp.Value.GetType(), itemElement, itemElement.Name.LocalName);
            else
                itemElement.SetValue(kvp.Value);

            propertyElement.Add(itemElement);
        }

        return propertyElement;
    }  
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.