Click here to Skip to main content
15,878,748 members
Articles / Programming Languages / XML

Using AutoMapper with Complex XML Data

Rate me:
Please Sign up or sign in to vote.
4.31/5 (8 votes)
6 Jan 2014CPOL3 min read 40K   977   14   7
Need to use automapper for non-trivial XML data? Here's how!

Introduction

AutoMapper is a very cool object-to-object mapping library that works great mapping POCO-to-POCO and even handles collections pretty seamlessly. Unfortunately, in the real world, sometimes the data we need comes to us in other formats. This article explores mapping XML data using AutoMapper.

Background

If you're like me, you found this article because you went searching for how to get AutoMapper to work with XML. If you did, you probably ran across the article by Danny Douglass on his blog. While this was extremely helpful, it only gets us part of the way there, as the source and target objects in this scenario are fairly simple. How would you handle more complicated XML data? Perhaps with nested types, or nested collections of types? This article picks up where that one left off and shows how to quickly and easily build a mapping for more complicated XML data.

At this point, some may be asking, "If it's XML data, why wouldn't you use XML serialization to solve the problem?" The answer, obviously, is that you could. However, in some circumstances, using object mapping may be easier, or you may have other constraints and considerations why you might want to use object mapping. Plus, it was an interesting problem to solve.

Using the Code

For our sample program here, we're going to consume an XML file which defines a nested menu structure. This menu supports menu items, and submenus, where each submenu in turn supports menu items and submenus. The file we're going to process looks like this:

XML
<?xml version="1.0" encoding="utf-8" ?>
<Menu>
  <SubMenus>
    <Menu name="SubMenu 1">
      <SubMenus>
        <Menu name="Sub-SubMenu 1">
          <MenuItems>
            <MenuItem name="Menu Item 1-1-1" action="Action1.exe" />
            <MenuItem name="Menu Item 1-1-2" action="Action2.exe" parameters="-2" />
          </MenuItems>
        </Menu>
        <Menu name="Sub-SubMenu 2">
          <MenuItems>
            <MenuItem name="Menu Item 1-2-1" action="Action3.exe" />
            <MenuItem name="Menu Item 1-2-2" action="Action4.exe" parameters="-2" />
          </MenuItems>
        </Menu>
      </SubMenus>
    </Menu>
    <Menu name="Sub Menu 2">
      <MenuItems>
        <MenuItem name="Menu Item 2-1" action="Action5.exe" parameters="-item 2.1" />
      </MenuItems>
    </Menu>
  </SubMenus>
  <MenuItems>
    <MenuItem name="Menu Item 1" action="Action6.exe"/>
    <MenuItem name="Menu Item 2" action="Action7.exe" parameters="-7" />
  </MenuItems>
</Menu>

The classes for the types we need are pretty straight forward from a modeling standpoint.  

C#
public class Menu
{
    public Menu()
    {
        SubMenus = new List<Menu>();
        MenuItems = new List<MenuItem>();
    }
    public IList<Menu> SubMenus { get; set; }
    public IList<MenuItem> MenuItems { get; set; }
    public string Name { get; set; }
}

public class MenuItem
{
    public string Name { get; set; }
    public string Action { get; set; }
    public string Parameters { get; set; }
}

At this point, AutoMapper needs to be told how to map the XML to these objects. At first, this might seem like it would be a lot of coding, but it's surprisingly small. Well, at least it was to me. YMMV. In the post from Danny Douglass, he shows a custom XElement resolver which he uses in his example. That's almost what's needed here, but since the sample XML has data as attributes instead of elements, a custom XAttributeResolver class is needed instead. As you can see in the snippet below, the name of the attribute is included in the constructor so the resolver knows which attribute to resolve.

C#
public class XAttributeResolver<T> : ValueResolver<XElement, T>
{
    public XAttributeResolver(string attributeName)
    {
        Name = attributeName;
    }

    public string Name { get; set; }

    protected override T ResolveCore(XElement source)
    {
        if (source == null)
            return default(T);
        var attribute = source.Attribute(Name);
        if (attribute == null || String.IsNullOrEmpty(attribute.Value))
            return default(T);

        return (T)Convert.ChangeType(attribute.Value, typeof(T));
    }
}

With the custom resolver complete, the maps can be built. We'll create a static <code>MapInitializer class with a CreateMenuMap method to contain the map construction logic. For the MenuItem class, the mapping is relatively straight forward - simply map the relevant attributes to the property names. The attribute name is passed to the XAttributeResolver using the ConstructedBy() member of the Mapper class.

C#
Mapper.CreateMap<XElement, MenuItem>()
      .ForMember(dest => dest.Name,
                 opt => opt.ResolveUsing<XAttributeResolver<string>>()
                           .ConstructedBy(() => new XAttributeResolver<string>("name")))
      .ForMember(dest => dest.Action,
                 opt => opt.ResolveUsing<XAttributeResolver<string>>()
                           .ConstructedBy(() => new XAttributeResolver<string>("action")))
      .ForMember(dest => dest.Parameters,
                 opt => opt.ResolveUsing<XAttributeResolver<string>>()
                           .ConstructedBy(() => new XAttributeResolver<string>("parameters")));

The Menu class is a bit trickier, as in addition to the Name property, there are nested Menu and MenuItem collections to deal with. Fortunately, given the structure of the XML data, it is possible to deal with both in a single delegate method. This method looks through descendants of the current node for a collection node, which contains the sub-element nodes. The delegate, then (with a little bit of error- prevention code), essentially flattens the source data and allows AutoMapper to walk the collection of XElement like it would any other collection.

XML
private static Func<XElement, string, 
    string, List<XElement>> _mapItems =
    (src, collectionName, elementName) =>
    (src.Element(collectionName) ?? 
    new XElement(collectionName)).Elements(elementName).ToList();

Armed now with this delegate, the Menu map is fairly straight forward to create using the MapFrom method.

XML
Mapper.CreateMap<XElement, Menu>()
      .ForMember(dest => dest.Name,
                 opt => opt.ResolveUsing<XAttributeResolver<string>>()
                           .ConstructedBy(() => new XAttributeResolver<string>("name")))
      .ForMember(dest => dest.MenuItems,
                 opt => opt.MapFrom(src => _mapItems(src, "MenuItems", "MenuItem")))
      .ForMember(dest => dest.SubMenus,
                 opt => opt.MapFrom(src => _mapItems(src, "SubMenus", "SubMenu")));

Putting it all together, the MapInitializer class looks like this:

C#
public static class MapInitializer
{
    private static Func<XElement, string, string, List<XElement>> _mapItems =
        (src, collectionName, elementName) =>
        (src.Element(collectionName) ?? new XElement(collectionName)).Elements(elementName).ToList();

    public static void CreateMenuMap()
    {
        //MenuItem map
        Mapper.CreateMap<XElement, MenuItem>()
                .ForMember(dest => dest.Name,
                           opt => opt.ResolveUsing<XAttributeResolver<string>>()
                                     .ConstructedBy(() => new XAttributeResolver<string>("name")))
                .ForMember(dest => dest.Action,
                           opt => opt.ResolveUsing<XAttributeResolver<string>>()
                                     .ConstructedBy(() => new XAttributeResolver<string>("action")))
                .ForMember(dest => dest.Parameters,
                           opt => opt.ResolveUsing<XAttributeResolver<string>>()
                                     .ConstructedBy(() => new XAttributeResolver<string>("parameters")));
        // Menu map
        Mapper.CreateMap<XElement, Menu>()
                .ForMember(dest => dest.Name,
                            opt =>
                            opt.ResolveUsing<XAttributeResolver<string>>()
                                  .ConstructedBy(() => new XAttributeResolver<string>("name")))
                .ForMember(dest => dest.MenuItems,
                            opt => opt.MapFrom(src => _mapItems(src, "MenuItems", "MenuItem")))
                .ForMember(dest => dest.SubMenus,
                            opt => opt.MapFrom(src => _mapItems(src, "SubMenus", "Menu")));
    }
}

At this point, everything we need to parse the XML should be ready to go. In my first pass, I did the XDocument.Load() and passed the results to Mapper.Map(). Pretty simple, right?

C#
MapInitializer.CreateMenuMap();
var xml = XDocument.Load(@".\Menu.xml");
var menu = Mapper.Map<XDocument, Menu>(xml);

Only one problem - the Menu resolver needs an XElement, not an XDocument. Doh! A quick tweak of the code:

C#
var menu = Mapper.Map<XElement,>(xml.Element("Menu"));

and some magic handwaving, the voila! - AutoMapper parses the XML and we can display an output of the menu.

> SubMenu 1
  > Sub-SubMenu 1
      Menu Item 1-1-1
      Menu Item 1-1-2
  > Sub-SubMenu 2
      Menu Item 1-2-1
      Menu Item 1-2-2
> Sub Menu 2
    Menu Item 2-1
  Menu Item 1
  Menu Item 2

Conclusion

Armed with the basics outlined in the article here, you should now have the tools you need to tackle almost any complex XML documents with AutoMapper.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionPlease, update to version 11 Pin
dhel19-Apr-22 21:36
dhel19-Apr-22 21:36 
GeneralAutoMapper 6.0.2 Update Pin
gus16925-May-17 7:26
gus16925-May-17 7:26 
GeneralMy vote of 4 Pin
mwpowellhtx2-Dec-14 14:01
mwpowellhtx2-Dec-14 14:01 
GeneralRe: My vote of 4 Pin
mwpowellhtx2-Dec-14 16:59
mwpowellhtx2-Dec-14 16:59 
The closest mapping *to* Xml I can seem to achieve is this:

C#
Mapper.CreateMap<Domain, XElement>()
.ProjectUsing(x => new XElement(@"Domain"));


C#
Domain d = new Domain();
var e = Mapper.Map<XElement>(d);


But this yields an exception. Had you had any luck with the opposite direction? Or possibly I am missing something about how AutoMapper handlers things like this? And/or how to go Linq to Xml and just avoid AM altogether?

AutoMapper.AutoMapperMappingException occurred
  HResult=-2146233088
  Message=

Mapping types:
String -> XName
System.String -> System.Xml.Linq.XName

Destination path:
XElement

Source value:
New Configuration
  Source=AutoMapper
  StackTrace:
       at Football.Data.Mappings.AutoMappingProvider.ToXml[T](T value) in i:\Source\Kingdom Software\Football\Simulator\Football.Data.Xml\Mappings\AutoMappingProvider.cs:line 19
  InnerException: System.Reflection.TargetInvocationException
       HResult=-2146232828
       Message=Exception has been thrown by the target of an invocation.
       Source=mscorlib
       StackTrace:
            at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
            at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
            at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
            at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
            at AutoMapper.Mappers.ImplicitConversionOperatorMapper.Map(ResolutionContext context, IMappingEngineRunner mapper)
            at AutoMapper.MappingEngine.AutoMapper.IMappingEngineRunner.Map(ResolutionContext context)
       InnerException: System.Xml.XmlException
            HResult=-2146232000
            Message=The ' ' character, hexadecimal value 0x20, cannot be included in a name.
            Source=System.Xml
            LineNumber=0
            LinePosition=4
            StackTrace:
                 at System.Xml.XmlConvert.VerifyNCName(String name, ExceptionType exceptionType)
                 at System.Xml.Linq.XNamespace.GetName(String localName, Int32 index, Int32 count)
                 at System.Xml.Linq.XName.Get(String expandedName)
            InnerException: 

GeneralRe: My vote of 4 Pin
mwpowellhtx2-Dec-14 17:16
mwpowellhtx2-Dec-14 17:16 
GeneralMy vote of 5 Pin
AlvoradaS7-Oct-14 22:45
AlvoradaS7-Oct-14 22:45 
GeneralMy vote of 3 Pin
SergioRykov15-Jan-14 2:18
SergioRykov15-Jan-14 2:18 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.