使用 XML 架构.NET Framework中的代码生成

 

丹尼尔·卡祖利诺

2004 年 5 月

适用于:
   Microsoft® Visual Studio® .NET

要求此下载需要安装 Microsoft .NET Framework 1.1。

总结: 了解类型化数据集与xsd.exe工具生成的类之间的差异,以及如何通过重用支持它的基础结构类来扩展此代码生成过程,同时保持与 XmlSerializer 的兼容性。 ) (23 页打印页

下载 代码示例

目录

简介
基础类
扩展 XSD 处理
内部 XmlSerializer
使用 CodeDom 进行自定义
映射提示
结论

简介

自动生成代码(无论是数据访问层、业务实体类,甚至是用户界面),都可以大大提高开发人员的工作效率。 此生成过程可以基于许多输入,例如数据库、任意 XML 文件、UML 关系图等。Microsoft® Visual Studio® .NET 内置支持从 W3C XML 架构文件生成代码, (XSD) 两种形式:类型化数据集和用于 XmlSerializer 的自定义类。

XSD 文件描述 XML 文档中允许的内容,以便根据文档被视为有效内容。 需要以类型安全的方式处理数据(最终将序列化为 XML 以供使用)导致将 XSD 转换为类的各种方法。 回想一下,XSD 不是作为描述对象及其关系的一种手段而创建的。 UML 已经存在更好的格式,它被广泛用于为应用程序建模并从中生成代码。 因此,.NET 与其面向对象的编程 (OOP) 概念与 XSD 概念之间存在一 (些预期) 不匹配。 在走 XSD -> 类映射的路径时,请记住这一点。

也就是说,CLR 类型系统可以视为 XSD 的子集:它支持无法映射到常规 OO 概念的功能。 如果仅使用 XSD 对类进行建模,而不是对文档进行建模,则几乎不会发现任何冲突。

在本文的其余部分,我们将讨论类型化数据集的方法、xsd.exe工具生成的自定义类如何生成更好的解决方案,以及如何扩展和自定义 XSD -> 类生成的输出。

若要充分利用本文,需要对 CodeDom 有基本的了解。

类型化数据集有什么问题?

类型化数据集越来越多地用于表示业务实体:也就是说,充当应用程序层之间的实体数据的传输,甚至作为 WebServices 的输出。 与“普通”数据集相反,类型化数据集很有吸引力,因为还可以获得对表、行、列等的类型化访问权限。但是,它们不是没有成本和/或限制的:

  • 实现开销:数据集包含实体可能不需要的许多功能,例如更改跟踪、类似 SQL 的查询、数据视图、大量事件等。
  • 性能:对 XML 进行序列化或从 XML 进行序列化的速度不是最快的。 XmlSerializer 轻松优于它。
  • 互操作性:non-.NET 返回类型化数据集的 WebServices 客户端可能会出现问题。
  • XML 结构:许多分层 (且完全有效的) 文档及其架构无法平展到表模型。

获取 有关类型化数据集的详细信息

除非数据集的其他功能通常与你相关,否则使用类型化数据集进行数据传递可能不是最佳选择。 幸运的是,还有另一个选项可以利用。

XmlSerializer 和自定义类

XmlSerializer 改进了 XML 数据处理的情景。 通过序列化属性, XmlSerializer 能够从其 XML 表示形式重新冻结对象,以及序列化回 XML 形式。 此外,它还能够以高效的方式执行此操作,因为它会生成一个动态编译的基于 XmlReader 的 (因此流式处理) 类专门针对 (de) 序列化具体类型。 也就是说,它确实非常快。

阅读有关 XML 序列化属性的详细信息

当然,猜测要使用哪些属性来符合 XSD 一点也不有趣。 为了解决此问题,.NET SDK 附带了一个实用工具,可为你完成艰苦的工作:xsd.exe。 它是一个命令行应用程序,能够从 XSD 文件生成类型化数据集和自定义类。 使用相应的 XML 序列化属性生成自定义类,以便在序列化时遵循架构的完全保真度。

阅读 Don Box 的 XSD 简介以及 CLR 映射和属性

到目前为止,一切顺利。 我们有一种高效且高性能的方式将 XML 转换为对象或从对象转换,并且我们有一个工具为我们生成类。 问题是,我们有时需要一些与生成的内容略有不同的东西。 例如,xsd.exe生成的类不能将数据绑定到Windows 窗体网格,因为它查找要显示的属性,而不是公共字段。 我们可能需要在此处和那里添加我们自己的自定义属性,将数组更改为类型化集合,等等。 当然,在序列化后保持与 XSD 兼容的同时,我们应该这样做。

自定义 XSD 显然会更改生成的类的形状。 如果你只是想将 PascalCase 转换为使用 camelCase 的事实 XML 标准,建议你三思而后行。 来自 MS 的即将推出的产品表明,它们将迁移到 PascalCase 中用于 XML 表示形式,以使其更易使用 .NET。

如果需要进一步的自定义(如上面提到的自定义项),你有什么选择? 人们几乎普遍认为,xsd.exe不可扩展,并且无法对其进行自定义。 这不准确,因为 .NET XML 团队实际上为我们提供了该工具使用的类。 你必须使用 CodeDom 脏,才能利用它们,但自定义级别仅受你的需求限制!

可以在以下文章中了解 CodeDom

以多种语言动态生成和编译源代码

使用 CodeDOM 以任何语言生成 .NET 代码

基础类

从 XSD 生成代码的一种方法是,只需循环访问架构对象模型 (SOM) ,然后以某种整体方式直接从该模型编写代码。 这是许多代码生成器为克服xsd.exe工具限制而创建的方法。 但是,这涉及到大量的工作量和代码,因为我们应考虑 XSD-CLR> 类型映射、XSD 类型继承、XML 序列化属性等。 掌握 SOM 也不是一项琐碎的任务。 如果我们可以添加或修改xsd.exe工具的内置代码生成,而不是自行完成所有操作,难道不是很好吗?

如我上面所说,与常见信念相反,xsd.exe用于生成输出的类是公开的, 可在System.Xml中使用。序列化 命名空间,即使xsd.exe工具当然不允许任何类型的自定义。 大多数情况下,它们确实是无证的,但我将在本部分中展示如何使用它们。 不要让 MSDN 中的以下语句让你生畏:“[TheTopSecretClassName] 类型支持 Microsoft® .NET Framework 基础结构,不应直接从代码中使用”。 我会在不使用黑客或任何反射代码的情况下使用它们。

与通常的 “StringBuilder.Append” 代码生成相比,一种更好的方法是利用 System.CodeDom 命名空间中的类,这正是内置代码生成类 (从现在开始) 代码 生成CodeDom 包含的类允许我们以与语言无关的方式以所谓的 AST (抽象语法树) 表示几乎任何编程构造。 稍后,另一个类(代码生成器)可以解释它并生成所需的原始代码,例如 Microsoft® Visual C# 或 Microsoft® Visual Basic.NET®。 这是大多数代码生成在 .NET Framework 中发生的方式。

codegen 方法不仅利用了这一点,而且架构分析和实际 CodeDom 生成也通过映射过程分离。 必须对要为其生成代码的每个架构元素执行此映射。 基本上,它会构造一个表示分析结果的新对象,例如其结构,该对象将是为其生成的类型名称、其成员、其 CLR 类型等。

为了使用这些类,我们遵循总结如下的基本工作流:

  1. 加载架构 (,原则上) 。
  2. 为每个顶级 XSD 元素派生一系列映射。
  3. 将这些映射导出到 System.CodeDom.CodeDomNamespace。

此过程涉及四个类,所有这些类都在System.Xml中定义。序列化命名空间:

图 1. 用于获取 CodeDom 树的类

可按如下所示获取使用这些类的 CodeDom 树:

namespace XsdGenerator
{
  public sealed class Processor
  {
    public static CodeNamespace Process( string xsdFile, 
       string targetNamespace )
    {
      // Load the XmlSchema and its collection.
      XmlSchema xsd;
      using ( FileStream fs = new FileStream( xsdFile, FileMode.Open ) )
      {
        xsd = XmlSchema.Read( fs, null );
        xsd.Compile( null );
      }
      XmlSchemas schemas = new XmlSchemas();
      schemas.Add( xsd );
      // Create the importer for these schemas.
      XmlSchemaImporter importer = new XmlSchemaImporter( schemas );
      // System.CodeDom namespace for the XmlCodeExporter to put classes in.
      CodeNamespace ns = new CodeNamespace( targetNamespace );
      XmlCodeExporter exporter = new XmlCodeExporter( ns );
      // Iterate schema top-level elements and export code for each.
      foreach ( XmlSchemaElement element in xsd.Elements.Values )
      {
        // Import the mapping first.
        XmlTypeMapping mapping = importer.ImportTypeMapping( 
          element.QualifiedName );
        // Export the code finally.
        exporter.ExportTypeMapping( mapping );
      }
      return ns;
    }
  }
}

尽管你可能想要在此处和那里添加异常管理代码,但代码非常简单。 需要注意的一点是,XmlSchemaImporter 使用其限定名称导入类型,然后该名称位于相应的 XmlSchema 中。 因此,架构中的所有全局元素都必须传递给它,这些元素使用 XmlSchema.Elements 集合进行迭代。 此集合以及 XmlSchemaElement.QualifiedName 是所谓的后架构编译信息集 (PSCI 的成员,请参阅 MSDN 帮助) ,该帮助在架构编译后填充。 它在解析引用、架构类型、继承、包含等后,具有填充和组织架构信息的效果。它在功能上类似于 DOM Post Validation Infoset (PSVI,请参阅 Dare Obasanjo 的 MSDN 文章XSD 规范) 。

你可能已经注意到一个副作用, (一个缺点实际上) XmlSchemaImporter 的工作方式:你只能检索 (导入) 全局定义的元素的映射。 架构中任何位置本地定义的任何其他元素都无法通过此机制进行访问。 这会产生一些后果,我稍后将讨论,这可能会限制可以应用的自定义或影响我们的架构设计。

XmlCodeExporter 类根据导入的映射使用类型定义填充传递给其构造函数的 CodeDomNamespace,生成所谓的 CodeDom 树。 上述方法生成的 CodeDom 正是xsd.exe工具在内部生成的。 有了此树,可以直接将其编译为程序集,也可以生成源代码。

如果想要摆脱xsd.exe工具,可以轻松生成使用此类的控制台应用程序。 为此,我需要从收到的 CodeDom 树生成源代码文件。 为此,我创建了一个适用于用户选择的目标语言的 CodeDomProvider:

static void Main( string[] args )
{
  if ( args.Length != 4 )
  {
    Console.WriteLine(
      "Usage: XsdGenerator xsdfile namespace outputfile [cs|vb]" );
    return;
  }
  // Get the namespace for the schema.
  CodeNamespace ns = Processor.Process( args[0], args[1] );
  // Create the appropriate generator for the language.
  CodeDomProvider provider;
  if ( args[3] == "cs" )
    provider = new Microsoft.CSharp.CSharpCodeProvider();
  else if ( args[3] == "vb" )
    provider = new Microsoft.VisualBasic.VBCodeProvider();
  else
    throw new ArgumentException( "Invalid language", args[3] );
  // Write the code to the output file.
  using ( StreamWriter sw = new StreamWriter( args[2], false ) )
  {
    provider.CreateGenerator().GenerateCodeFromNamespace(
      ns, sw, new CodeGeneratorOptions() );
  }
  Console.WriteLine( "Finished" );
  Console.Read();

}

可以使用生成器收到的 CodeGeneratorOptions 实例的属性进一步自定义生成的代码格式和其他选项。 有关可用选项,请查看 MSDN 文档

编译此控制台应用程序后,我可以生成与 xsd.exe 工具中的代码完全相同的代码。 拥有此功能使我完全无需依赖该工具,我不再需要知道它是否已安装或位于何处,也无需启动新流程等。但是,每次修改架构时,从命令行一遍又一遍地运行它远非理想。 Microsoft® Visual Studio.NET® 允许开发人员通过所谓的自定义工具利用设计时代码生成。 一个示例是类型化数据集,其中 (尽管无需指定它) 自定义工具在每次保存数据集 XSD 文件时都会处理它,但自动生成相应的“代码隐藏”类。

生成自定义工具不在本文章的范围内,但你可以阅读有关将我到目前为止编写的代码转换为 此博客文章的详细信息。 该工具的代码包含在本文下载中,只需将“XsdCodeGen”自定义工具名称分配给 XSD 文件属性即可使用它。 随附的自述文件中介绍了注册。

即使我去获取易于使用的自定义工具,将xsd.exe工具替换为另一个执行完全相同的工具看起来并不引人注目,是吗? 毕竟,我们之所以开始这一切,就是为了改变这种行为! 因此,让我们继续从此基线自定义它。

扩展 XSD 处理

为了自定义处理,我需要将信息传递到工具,以便工具知道要更改或处理的内容。 此处有两个主要选项:

  • 将 (许多可能) 属性添加到 XSD 根 <xs:schema> 元素中,处理器可以理解这些属性来应用自定义项,类似于类型化数据集方法。

    有关此内容的详细信息,请参阅此处

  • 通过架构注释使用内置的 XSD 扩展性,以允许任意自定义。 它只是将类型添加到一种代码生成管道中,以在基本生成发生后执行。

第一种方法最初很吸引人,因为它的简单性。 我只需添加属性并相应地修改处理器,使其检查:

架构:

<xs:schema elementFormDefault="qualified" 
  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns:code="https://weblogs.asp.net/cazzu"
  code:fieldsToProperties="true">

代码:

XmlSchema xsd;
// Load the XmlSchema.
...
foreach (XmlAttribute attr in xsd.UnhandledAttributes)
{
  if (attr.NamespaceURI == "https://weblogs.asp.net/cazzu")
  {
    switch (attr.LocalName)
    {
      case "fieldsToProperties":
        if (bool.Parse(attr.Value)) ConvertFieldsToProperties(ns);
        break;
      ...
    }
  }
}

这是通常会在其他 xsd 类>生成器中看到的方法, (可以在 代码生成网络) 找到大量它们。 遗憾的是,此方法会导致长开关语句、无休止的属性,并最终导致代码无法维护以及缺乏扩展性。

第二种方法更可靠,因为它从一开始就考虑了扩展性。 XSD 通过 <xs:annotation> 元素提供此类扩展功能,该元素可以是架构中几乎任何项的子元素。 我将利用它及其 <xs:appinfo> 子元素,以允许开发人员指定要运行哪些 (任意) 扩展以及按哪种顺序运行。 此类扩展架构如下所示:

<xs:schema elementFormDefault="qualified"
           xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:annotation>
    <xs:appinfo>
      <Code xmlns="https://weblogs.asp.net/cazzu">
        <Extension 
Type="XsdGenerator.Extensions.FieldsToPropertiesExtension, 
XsdGenerator.CustomTool" />
      </Code>
    </xs:appinfo>
  </xs:annotation>

当然,每个扩展都需要实现一个通用接口,以便自定义工具可以轻松执行每个接口:

public interface ICodeExtension
{
  void Process( System.CodeDom.CodeNamespace code, 
                System.Xml.Schema.XmlSchema schema );
}

通过预先提供此类扩展性,很容易随着新自定义需求的出现而向前推进。 即使是最基本的扩展,也可以从一开始就实现为扩展。

可扩展代码生成工具

我将修改 Processor 类以允许此新功能,并且只需从架构中检索每个 <Extension> 元素。 不过,此处有一个警告:与为元素、属性、类型等公开的架构编译后信息集属性不同,架构级别没有用于批注的类型化属性。 也就是说,没有 XmlSchema.Annotations 属性。 因此,需要迭代 XmlSchema.Items 泛型预编译属性来查找批注。 此外,在检测到 XmlSchemaAnnotation 项后,需要再次迭代其自己的 Items 泛型集合,因为可能存在 <xs:appinfo> 以及 <xs:documentation> 子项,它们也缺少类型化属性。 当最终通过 XmlSchemaAppInfo.Markup 属性到达 appinfo 的内容时,我们只需获得一个 XmlNode 对象数组。 你可以想象故事是怎么去的:循环访问节点、再次循环访问其子级等。 这使得代码非常丑陋。

幸运的是,XSD 文件只不过是一个 XML 文件;因此,可以使用 XPath 来查询它。

为了加快执行速度,我将在其静态构造函数中为 XPath 保留一个静态编译表达式,该表达式在 Processor 类中初始化:

public sealed class Processor
{
  public const string ExtensionNamespace = "https://weblogs.asp.net/cazzu";
  private static XPathExpression Extensions;
  static Processor() 
  {
    XPathNavigator nav = new XmlDocument().CreateNavigator();
    // Select all extension types.
    Extensions = nav.Compile
    ("/xs:schema/xs:annotation/xs:appinfo/kzu:Code/kzu:Extension/@Type");
    
    // Create and set namespace resolution context.
    XmlNamespaceManager nsmgr = new XmlNamespaceManager(nav.NameTable);
    nsmgr.AddNamespace("xs", XmlSchema.Namespace);
    nsmgr.AddNamespace("kzu", ExtensionNamespace);
    Extensions.SetContext(nsmgr);
  }

请注意 ,有关 XPath 预编译和执行的优点、详细信息和高级应用程序的详细信息,请参阅 高性能 XML (I) :动态 XPath 表达式编译高性能 XML (II) :XPath 执行提示

Process () 方法需要执行此查询,并执行找到的每个 ICodeExtension 类型,然后将 CodeNamespace 返回给调用方:

XPathNavigator nav;
using ( FileStream fs = new FileStream( xsdFile, FileMode.Open ) )
{ nav = new XPathDocument( fs ).CreateNavigator(); }
XPathNodeIterator it = nav.Select( Extensions );
while ( it.MoveNext() )
{
  Type t = Type.GetType( it.Current.Value, true );
  // Is the type an ICodeExtension?
  Type iface = t.GetInterface( typeof( ICodeExtension ).Name );
  if (iface == null)
    throw new ArgumentException( "Invalid extension type '" + 
       it.Current.Value + "'." );
  ICodeExtension ext = ( ICodeExtension ) Activator.CreateInstance( t );
  // Run it!
  ext.Process( ns, xsd );
}
return ns;

我使用的是 Type.GetInterface () 来测试接口实现,而不是 Type.IsAssignableFrom () ,因为它的开销似乎更少,因为它可以快速跳转到非托管代码。 但是,效果是相同的,后者返回布尔值而不是类型 (,如果) 找不到接口,则返回 null。

内部 XmlSerializer

拥有 CodeDom 为寻找自定义项的开发人员带来了很大的功能和灵活性,但这也带来了更大的责任。 修改代码时存在以下风险:代码将停止以与架构兼容的方式序列化,或者 XmlSerializer 功能完全中断,意外节点和属性引发异常、无法检索值等。

因此,在使用生成的代码之前,绝对需要了解 XmlSerializer 的内部特性,并且肯定需要一种方法来了解幕后发生的情况。

当对象即将进行 XML 序列化时,会通过反映传递给 XmlSerializer 构造函数的类型来创建临时程序集, (这就是必须在) 执行此操作的原因。 等! 不要因为“反映”这个词而惊慌失措! 每个类型仅执行一次此操作,并且会创建一对极其高效的读取器和编写器类,以在 AppDomain 的生命周期内处理序列化和反序列化。

这些类继承System.Xml中的公共 XmlSerializationReader 和 XmlSerializationWriter 类。序列化命名空间。 它们也是 [TheTopSecretClassName]。 如果要查看这些动态生成的类,只需将以下设置添加到 Web 应用程序的应用程序配置文件 (web.config) :

<system.diagnostics>
  <switches>
    <add name="XmlSerialization.Compilation" value="4"/>
  </switches>
</system.diagnostics>

现在,序列化程序不会删除进程中生成的临时文件。 对于 Web 应用程序,这些文件将位于 C:\Documents and Settings\[YourMachineName]\ASPNET\Local Settings\Temp;否则,它们将位于当前用户本地设置\Temp 文件夹中。

你将看到的代码正是想要在 .NET 中有效加载 XML 时必须执行的操作:使用嵌套;如果在阅读时使用 XmlReader 方法,则使用 XmlReader 方法向下移动流,等等。所有丑陋的代码都是为了使其非常快。

还可以使用 Chris Sells 的 XmlSerializerPreCompiler 工具诊断这些生成的类中的问题。

我们可以查看此代码,以分析序列化程序生成的类中更改的影响。

使用 CodeDom 进行自定义

许多自定义项将立即吸引人,因为它们是xsd.exe工具生成的类的常规问题。

将字段转换为属性

大多数开发人员抱怨的一个问题是,xsd.exe工具生成具有公共字段的类,而不是由专用字段支持的属性。 XmlSerializer 生成的类使用常规 [object] 从类的实例中读取和写入值。[member] 表示法。 当然,从编译和源代码的角度来看,[成员]是字段还是属性没有区别。

因此,在 CodeDom 的帮助下,可以更改 XSD 的默认类。 由于自定义 codegen 工具内置的扩展性,只需实现新的 ICodeExtension 即可。 扩展将处理 CodeDom 树中的每个类型,无论是类还是结构:

public class FieldsToPropertiesExtension : ICodeExtension
{
  #region ICodeExtension Members
  public void Process( System.CodeDom.CodeNamespace code, 
                       System.Xml.Schema.XmlSchema schema )
  {
    foreach ( CodeTypeDeclaration type in code.Types )
    {
      if ( type.IsClass || type.IsStruct )
      {
         // Turn fields to props

现在,我需要循环访问类型的每个成员, (可以是字段、属性、方法等) ,并且仅处理 CodeMemberField 成员。 我不能简单地对类型执行 foreach。但是,成员集合,因为对于每个字段,我需要将属性添加到同一集合。 这将导致异常,因为 foreach 构造使用的基础枚举器将变为无效。 因此,我需要将当前成员复制到数组中,并改为循环访问数组:

CodeTypeMember[] members = new CodeTypeMember[type.Members.Count];
type.Members.CopyTo( members, 0 );
foreach ( CodeTypeMember member in members )
{
  // Process fields only.
  if ( member is CodeMemberField )
  {
    // Create property

接下来,创建新属性:

CodeMemberProperty prop = new CodeMemberProperty();
prop.Name = member.Name;
prop.Attributes = member.Attributes;
prop.Type = ( ( CodeMemberField )member ).Type;
// Copy attributes from field to the property.
prop.CustomAttributes.AddRange( member.CustomAttributes );
member.CustomAttributes.Clear();
// Copy comments from field to the property.
prop.Comments.AddRange( member.Comments );
member.Comments.Clear();
// Modify the field.
member.Attributes = MemberAttributes.Private;
Char[] letters = member.Name.ToCharArray();
letters[0] = Char.ToLower( letters[0] );
member.Name = String.Concat( "_", new string( letters ) );

请注意,我将字段名称、成员属性和类型复制到新属性。 将 xmlSerialization 属性 (注释和自定义属性) 移出字段,移动到 属性 (AddRange () Clear () ) 。 最后,我将字段设为私有,并将其第一个字母转换为小写,并在前面附加“_”字符,这是属性支持字段的一种相当常见的命名约定。

属性中最重要的部分仍然缺失:其 get 和 set 访问器实现。 由于它们只是一个传递到字段值的传递,所以它们非常简单:

prop.HasGet = true;
prop.HasSet = true;
// Add get/set statements pointing to field. Generates:
// return this._fieldname;
prop.GetStatements.Add( 
  new CodeMethodReturnStatement( 
    new CodeFieldReferenceExpression( 
      new CodeThisReferenceExpression(), member.Name ) ) );
// Generates:
// this._fieldname = value;
prop.SetStatements.Add(
  new CodeAssignStatement(
    new CodeFieldReferenceExpression( 
      new CodeThisReferenceExpression(), member.Name ), 
    new CodeArgumentReferenceExpression( "value" ) ) );

最后,我们只需将新属性添加到 类型:

  type.Members.Add( prop );
}

现在,以前的架构通过 工具生成以下内容:

/// <remarks/>
[System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=false)]
public class Publisher
{
  /// <remarks/>
  public string pub_id;

将相应的扩展添加到架构后:

<xs:schema elementFormDefault="qualified"  
xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:annotation>
    <xs:appinfo>
      <Code xmlns="https://weblogs.asp.net/cazzu">
        <Extension 
Type="XsdGenerator.Extensions.FieldsToPropertiesExtension,
       XsdGenerator.CustomTool" /> 
      </Code>
    </xs:appinfo>
  </xs:annotation>
  ...

现在将生成:

/// <remarks/>
[System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=false)]
public class Publisher
{
  private string _pub_id;
  /// <remarks/>
  public string pub_id
  {
    get
    {
      return this._pub_id;
    }
    set
    {
      this._pub_id = value;
    }
  }

使用集合而不是数组

为了使任何体面的读取和写入 (具有 get+set 属性) 对象模型易于程序员使用,其多值属性应基于集合,而不是基于数组。 这样做可以更轻松地修改值和操作对象图。 通常的方法涉及从 CollectionBase 派生新的类型化集合类。

在提交更改 CodeDom 之前,必须检查 XmlSerializer 支持集合。 在分析和反映要序列化的类型的类的深处,有一个名为 TypeScope 的内部类。 TypeScope 负责确保可以生成序列化代码。 如果 包含有趣的方法 ImportTypeDesc,它将执行大多数检查并生成受支持类型的信息。 下面是对 IXmlSerializable 的特殊支持, (它检查其成员) 、数组 (的排名必须等于 1) 、 EnumsXmlNodeXmlAttributeXmlElement 等。

具体而言,对于集合,导入方法会检查实现 ICollection 的类型,这些类型必须满足以下规则:

  • 必须具有 Add 方法,该方法不是由 接口定义的,因为它通常是为集合将保留的专用类型创建的。
  • 不得通过集合实现 IDictionary
  • 必须具有默认成员 (即索引器) 具有类型为 System.Int32 (C# int) 的单个参数。 将在所有类型层次结构中搜索此类成员。
  • Add、Count 和 indexer 中不得有任何安全属性。

验证此信息后,生成的专用 XmlSerializationWriter 派生类在为类型编写 XML 输出时使用 Count 属性循环访问,而不是针对基于数组的属性使用 Lenth:

MyAssembly.MyCollection a = (MyAssembly.MyCollection)o.@CollectionProperty;
if (a != null) {
    for (int ia = 0; ia < a.Count; ia++) {
        Write10_MyCollectionItem(@"MyCollectionItem", 
          @"https://weblogs.asp.net/cazzu/", 
          ((MyAssembly.MyCollectionItem)a[ia]), false, false);
    }
}

请注意,鉴于索引器上以前的检查,对集合和数组的索引访问是相同的,因此没有更改。

相应的 XmlSerializationReader 派生类使用类型化的 Add 方法填充集合:

MyAssembly.MyCollection a_2 = (MyAssembly.MyCollection)o.@CollectionProperty;
...
while (Reader.NodeType != System.Xml.XmlNodeType.EndElement) 
{
  if (Reader.NodeType == System.Xml.XmlNodeType.Element) 
  {
    if (((object) Reader.LocalName == (object)id8_MyCollectionItem && 
       (object) Reader.NamespaceURI == (object)id9_httpweblogsaspnetcazzu)) 
    {
      if ((object)(a_2) == null) 
        Reader.Skip(); 
      else 
        a_2.Add(Read10_MyCollectionItem(false, true));
    }
    ...

上面所示的 read 方法返回集合所需的适当类型:

MyAssembly.MyCollectionItem Read1_MyCollectionItem(bool isNullable, 
  bool checkType)

现在, 已检查 XmlSerializer 对基于集合的属性的支持和正确处理,可以安全地将所有数组更改为相应的强类型集合。

此新扩展可以设计为在上一个扩展之前或之后运行。 差异很大,因为迭代将分别从字段更改为新属性。 为了使此扩展独立于上一个扩展,我将对其进行编码以针对字段工作。 但请注意,如果配置为在 FieldsToPropertiesExtension 之后运行,则代码将不正确。

让我们首先分析将生成自定义集合的方法。 集合应如下所示:

public class PublisherCollection : CollectionBase
{
  public int Add(Publisher value)
  {
    return base.InnerList.Add(value);
  }
  public Publisher this[int idx]
  {
    get { return (Publisher) base.InnerList[idx]; }
    set { base.InnerList[idx] = value; }
  }
}

生成此类型化集合的代码为:

public CodeTypeDeclaration GetCollection( CodeTypeReference forType )
{
  CodeTypeDeclaration col = new CodeTypeDeclaration( 
    forType.BaseType + "Collection" );
  col.BaseTypes.Add(typeof(CollectionBase));
  col.Attributes = MemberAttributes.Final | MemberAttributes.Public;
  // Add method
  CodeMemberMethod add = new CodeMemberMethod();
  add.Attributes = MemberAttributes.Final | MemberAttributes.Public;
  add.Name = "Add";
  add.ReturnType = new CodeTypeReference(typeof(int));
  add.Parameters.Add( new CodeParameterDeclarationExpression (
    forType, "value" ) );
  // Generates: return base.InnerList.Add(value);
  add.Statements.Add( new CodeMethodReturnStatement (
    new CodeMethodInvokeExpression( 
      new CodePropertyReferenceExpression( 
        new CodeBaseReferenceExpression(), "InnerList"), 
      "Add", 
      new CodeExpression[] 
        { new CodeArgumentReferenceExpression( "value" ) } 
      )
    )
  );
  // Add to type.
  col.Members.Add(add);
  // Indexer property ('this')
  CodeMemberProperty indexer = new CodeMemberProperty();
  indexer.Attributes = MemberAttributes.Final | MemberAttributes.Public;
  indexer.Name = "Item";
  indexer.Type = forType;
  indexer.Parameters.Add( new CodeParameterDeclarationExpression (
    typeof( int ), "idx" ) );
  indexer.HasGet = true;
  indexer.HasSet = true;
  // Generates: return (theType) base.InnerList[idx];
  indexer.GetStatements.Add( 
    new CodeMethodReturnStatement (
      new CodeCastExpression( 
        forType, 
        new CodeIndexerExpression( 
          new CodePropertyReferenceExpression( 
            new CodeBaseReferenceExpression(), 
            "InnerList"), 
          new CodeExpression[] 
            { new CodeArgumentReferenceExpression( "idx" ) } ) 
        )
      )
    );
  // Generates: base.InnerList[idx] = value;
  indexer.SetStatements.Add( 
    new CodeAssignStatement( 
      new CodeIndexerExpression( 
        new CodePropertyReferenceExpression( 
          new CodeBaseReferenceExpression(), 
          "InnerList"), 
        new CodeExpression[] 
          { new CodeArgumentReferenceExpression("idx") }), 
      new CodeArgumentReferenceExpression( "value" )
    )
  );
  // Add to type.
  col.Members.Add(indexer);
  return col;
}

此时,在对 CodeDom 进行编程时,应考虑一个有用的提示;看到那些看似没完没了的 语句。添加行? 当然,我们可以将它们分解成单独的行,每个行创建一个临时变量来保存对象并传递给下一个对象。 这将使他们更加无尽! 那么,一旦你习惯了它,下面的提示是将这些线分解成其片段的好方法:

若要生成 CodeDom 嵌套语句,通常从右到左构造连续的属性/索引器/方法访问。

在实践中:生成以下内容:

base.InnerList[idx]

从索引器表达式 [idx] 开始,继续访问属性 InnerList,然后以对象引用基结束。 这适用于以下 CodeDom 嵌套语句:

CodeExpression st = new CodeIndexerExpression( 
  new CodePropertyReferenceExpression( 
    new CodeBaseReferenceExpression(), 
    "InnerList"
  ), 
  new CodeExpression[] 
    { new CodeArgumentReferenceExpression( "idx" ) } 
);

请注意,我从右到左创建 语句,最后完成相应的构造函数参数。 通常,最好手动缩进和拆分行,以便更轻松地查看每个对象构造函数的结束位置,以及哪些是其参数。

最后,ICodeExtension.Process 方法实现涉及循环访问类型及其字段,以查找基于数组的类型:

public class ArraysToCollectionsExtension : ICodeExtension
{
  public void Process( CodeNamespace code, XmlSchema schema )
  {
    // Copy as we will be adding types.
    CodeTypeDeclaration[] types = 
      new CodeTypeDeclaration[code.Types.Count];
    code.Types.CopyTo( types, 0 );
    foreach ( CodeTypeDeclaration type in types )
    {
      if ( type.IsClass || type.IsStruct )
      {
        foreach ( CodeTypeMember member in type.Members )
        {
          // Process fields only.
          if ( member is CodeMemberField && 
            ( ( CodeMemberField )member ).Type.ArrayElementType != null )
          {
            CodeMemberField field = ( CodeMemberField ) member;
            CodeTypeDeclaration col = GetCollection( 
              field.Type.ArrayElementType );
            // Change field type to collection.
            field.Type = new CodeTypeReference( col.Name );
            code.Types.Add( col );
          }
        }
      }
    }
  }

和以前一样,我复制了需要修改的集合;在本例中为 CodeNamespace.Types。

进一步的自定义可能包括将 [Serializable] 添加到生成的类,添加 DAL 方法 (即 LoadByIdFindByKeySaveDelete 等) ,生成序列化忽略的成员,由代码 (应用 XmlIgnoreAttribute) ,省略属于外部导入架构的类的生成,等等。

映射提示

如果要深入了解代码生成工具本身,或者想要进一步自定义架构处理,则可能对 codegen 类的以下高级问题感兴趣。 如果你只打算开发扩展并操作 CodeDom,它们不会为你增加太多价值,你可以安全地跳过此部分。

我通过检索元素的 XmlTypeMapping 处理了元素;我没有使用过它的任何属性,但如果必须找到对应于元素的 CodeTypeDeclaration,则可能需要它们。 可以在 MSDN 文档中找到 XmlTypeMapping 属性及其含义的简要说明。 但是,此类用于许多方案,如文档中所示的 SoapReflectionImporter 映射导入。 至于我正在使用的 XmlSchemaImporter,我发现 XmlTypeMapping.TypeFullName 和 XmlTypeMapping.TypeName 在一个特定架构元素设计中的行为不正确:如果它在序列中包含单个未绑定的子元素,则两者都错误地假定子属性的类型。

因此,对于以下架构元素:

<xs:element name="pubs">
  <xs:complexType>
    <xs:sequence>
      <xs:element name="publishers" type="Publisher" maxOccurs="unbounded" />
    </xs:sequence>
  </xs:complexType>
</xs:element>

XmlTypeMapping.TypeFullName 和 XmlTypeMapping.TypeName 都具有值“Publisher[]”,这是其唯一属性的类型,而不是将生成的类型“pubs”值。 如果序列具有多个元素,则一切将按预期工作。 请注意,无论元素的类型是否为命名的全局类型,或者元素本身是否为引用,此 (明显的) bug 都适用。

除了类型映射之外,XmlSchemaImporter 还可以检索将应用于其成员的映射 (字段) 。 这非常有用,因为 XSD/CLR 类型映射(包括 XSD 自定义派生类型)已解析,你可以确保它是 XmlSerializer 使用的适当类型。 可以按如下所示获取成员映射:

XmlMembersMapping mmap = importer.ImportMembersMapping( 
  element.QualifiedName );
int count = mmap.Count;
for (int i = 0; i < count; i++)
{
  XmlMemberMapping map = mmap[i];
  //You have now: 
  //  map.ElementName
  //  map.MemberName
  //  map.TypeFullName
  //  map.TypeName
}

XmlMemberMapping.TypeFullName 保存命名空间限定的 CLR 类型,而 XmlMemberMapping.TypeName 具有 XSD 类型名称。 例如,对于 XSD 类型“xs:positiveInteger”的成员,前者为“System.String”,后者为“positiveInteger”。如果无权访问此成员映射检索,则必须知道 XmlSerializer 使用的所有 XSD 到 CLR 类型转换规则。 请注意,这些规则不一定用于 XSD 验证和 DOM PSVI。

再次 (有一个重要的注意事项,显然成员导入) bug。 不能重复使用 XmlSchemaImporter,或者会在构造时某个位置 XmlMembersMapping 收到导入代码引发的 InvalidCastException。 这可以通过每次使用导入程序的新实例来解决。

有了此信息,就可以完全更改类外观(例如,重命名属性以使第一个字母大写),而不会使序列化基础结构面临风险。

我在讨论 codegen 类的基础时说,你只能检索 (导入) 全局定义元素的映射;如果创建自己的自定义属性来修改生成的类,则只能检索和分析顶级元素,因为只有这些元素的映射。 例如,假设你添加了 code:className 属性,该属性由某个扩展用来更改生成的类名称:

<xs:schema xmlns:code="https://weblogs.asp.net/cazzu" ...>
  <xs:element name="pubs" code:className="PublishersData">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="publishers" code:className="Publishers">
          <xs:complexType>

你将能够检索 pubs 元素的映射,但不能检索 publishers 子元素的映射。 因此,处理它并不安全,因为 codegen 类将来可能会更改。 如果没有当前映射,则不能简单地假设相应的 CodeTypeDeclaration 将与元素同名 (,以便查找它并) 更改它。 当然,你可以承担风险作为可接受的风险。

结论

重用为 XmlSerializer 创建的内置代码生成功能可确保对生成的代码进行轻微更改不会中断 XML 序列化。 通过 CodeDom 直接操作其输出也是提高灵活性的一个优势。 我演示了 XML 架构的灵活处理如何允许任意扩展更改输出,并开发了一些有用的示例。

有了这一坚实的基础,你可以迁移到更高级的方案;外部 (导入/包含) XSD 架构及其与代码生成的关系、操作代码输出以重用 XSD 和应用程序或公司范围的存储库中的相应生成的 .NET 类型) (类型定义,等等。

我希望这是你使用新方法进行 XSD 存储和管理以及相应代码生成和重用的开始。