使用 AutoMapper 处理复杂的 XML 数据






4.31/5 (8投票s)
需要为非平凡的 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
属性之外,还有嵌套的 Menu
和 MenuItem
集合需要处理。幸运的是,考虑到 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 文档所需的工具。