65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 AutoMapper 处理复杂的 XML 数据

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.31/5 (8投票s)

2014年1月6日

CPOL

3分钟阅读

viewsIcon

41960

downloadIcon

1008

需要为非平凡的 XML 数据使用 automapper?方法在此!

引言

AutoMapper 是一个非常酷的 对象到对象 映射库,它非常适合 POCO 到 POCO 的映射,甚至可以无缝处理集合。不幸的是,在现实世界中,有时我们需要的数据以其他格式提供给我们。本文探讨了使用 AutoMapper 映射 XML 数据。

背景

如果你像我一样,你找到这篇文章是因为你搜索了如何让 AutoMapper 与 XML 一起工作。如果你这样做了,你可能偶然发现了 Danny Douglass 在他的博客上发表的文章。虽然这非常有帮助,但它只帮助我们走了一半的路,因为在这种情况下源对象和目标对象都相当简单。您将如何处理更复杂的 XML 数据?也许有嵌套的类型,或者嵌套的类型集合?本文接续了那篇文章的内容,展示了如何快速轻松地构建更复杂的 XML 数据的映射。

在这一点上,有些人可能会问:“如果是 XML 数据,为什么不使用 XML 序列化来解决这个问题?” 答案很明显,你可以这样做。但是,在某些情况下,使用对象映射可能更容易,或者您可能有其他约束和考虑因素导致您可能希望使用对象映射。此外,这是一个有趣的待解决问题。

Using the Code

对于我们这里的示例程序,我们将使用一个定义嵌套菜单结构的 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>

从建模的角度来看,我们需要的类型的类非常简单。  

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; }
}

在这一点上,需要告诉 AutoMapper 如何将 XML 映射到这些对象。起初,这看起来会是很多代码,但它出奇地小。嗯,至少对我来说是这样。 YMMV。 在 Danny Douglass 的文章中,他展示了一个自定义的 XElement 解析器,他在他的示例中使用它。 这几乎是这里需要的,但是由于示例 XML 将数据作为属性而不是元素,因此需要一个自定义的 XAttributeResolver 类。 如下面的代码片段所示,属性的名称包含在构造函数中,以便解析器知道要解析哪个属性。

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

完成自定义解析器后,可以构建映射。我们将创建一个 static MapInitializer 类,其中包含一个 CreateMenuMap 方法来包含映射构建逻辑。对于 MenuItem 类,映射相对简单 - 仅将相关属性映射到属性名称。属性名称使用 Mapper 类的 ConstructedBy() 成员传递给 XAttributeResolver

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 类稍微复杂一些,因为除了 Name 属性之外,还有嵌套的 MenuMenuItem 集合需要处理。幸运的是,考虑到 XML 数据的结构,可以在单个委托方法中同时处理两者。此方法搜索当前节点的后代,以查找一个集合节点,该节点包含子元素节点。然后,委托(带有一些错误预防代码)本质上会展平源数据,并允许 AutoMapper 遍历 XElement 的集合,就像遍历任何其他集合一样。

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

现在有了这个委托,使用 MapFrom 方法创建 Menu 映射非常简单。

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")));

将所有内容放在一起,MapInitializer 类如下所示

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")));
    }
}

在这一点上,我们需要解析 XML 的所有内容都应该准备就绪。在我的第一遍中,我做了 XDocument.Load() 并将结果传递给 Mapper.Map()。很简单,对吧?

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

只有一个问题 - Menu 解析器需要一个 XElement,而不是一个 XDocument。 哎呀! 快速调整代码

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

以及一些神奇的手势,瞧! - AutoMapper 解析 XML,我们可以显示菜单的输出。

> 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

结论

有了这里文章中概述的基础知识,您现在应该拥有使用 AutoMapper 处理几乎任何复杂 XML 文档所需的工具。

© . All rights reserved.