XmlCodeExporter 和可为空的类型

Posted

技术标签:

【中文标题】XmlCodeExporter 和可为空的类型【英文标题】:XmlCodeExporter and nullable types 【发布时间】:2017-07-06 19:34:58 【问题描述】:

System.Xml.Serialization.XmlCodeExporter 从 XSD 模式生成代码(以代码 CodeDom 形式)。但它有一些怪癖。例如一个可选元素:

<xs:element name="Something" type="xs:decimal" minOccurs="0" maxOccurs="1"/>

我希望这会生成一个相应的Nullable&lt;decimal&gt; 类型的代码成员,但它实际上创建了一个decimal 类型的成员,然后是一个单独的SomethingSpecified 字段,该字段应该单独切换以指示空值。这可能是因为该库是在引入可空类型之前开始的,但它导致代码非常不方便。

是否可以调整此代码生成,或者在这种情况下是否有替代工具可以生成更好的代码?

编辑:我知道我可以修改架构并添加 nillable='true',但我不想更改架构以解决代码生成的限制。

【问题讨论】:

试试 Xsd2Code 工具。我广泛使用它而不是 xsd.exe。不幸的是,我无法确定您提到的那个特定问题是否在那里得到了解决。 您是否尝试过使用nillable='true' 代替 min/max 出现? @Evk:Xsd2Code 似乎在后台使用 XmlCodeExporter 并且似乎有同样的问题。 @tchrikch:是的,在架构中使用 nillable="true" 会生成可为空的属性,但我不想更改架构以解决代码生成中的限制,因为架构是共享合约。 @JacquesB 是的,我猜在最坏的情况下,您可以在解析之前预处理 xsd 模式,并在可行的情况下将 max/min 定义替换为 nillable 【参考方案1】:

Mike Hadlow 的文章 Writing your own XSD.exe 提供了创建您自己的 xsd.exe 版本的基本框架。它有以下步骤:

    使用XmlSchema.Read()XmlSchemaImporter 导入架构。

    使用XmlCodeExporter生成要创建的.Net类型和属性。

    根据需要调整生成的类型和属性

    在这里,您需要删除生成的 xxxSpecified 属性并将其对应的“真实”属性提升为可为空。

    使用CSharpCodeProvider生成最终代码。

使用这个框架,并通过使用调试器通过实验确定XmlCodeExporter 实际生成的类型,我创建了以下CustomXsdCodeGenerator

public class CustomXsdCodeGenerator : CustomXsdCodeGeneratorBase

    readonly bool promoteToNullable;

    public CustomXsdCodeGenerator(string Namespace, bool promoteToNullable) : base(Namespace)
    
        this.promoteToNullable = promoteToNullable;
    

    protected override void ModifyGeneratedCodeTypeDeclaration(CodeTypeDeclaration codeType, CodeNamespace codeNamespace)
    
        RemoveSpecifiedProperties(codeNamespace, promoteToNullable);
        base.ModifyGeneratedCodeTypeDeclaration(codeType, codeNamespace);
    

    private static void RemoveSpecifiedProperties(CodeNamespace codeNamespace, bool promoteToNullable)
    
        foreach (CodeTypeDeclaration codeType in codeNamespace.Types)
        
            RemoveSpecifiedProperties(codeType, codeNamespace, promoteToNullable);
        
    

    private static void RemoveSpecifiedProperties(CodeTypeDeclaration codeType, CodeNamespace codeNamespace, bool promoteToNullable)
    
        var toRemove = new List<CodeTypeMember>();

        foreach (var property in codeType.Members.OfType<CodeMemberProperty>())
        
            CodeMemberField backingField;
            CodeMemberProperty specifiedProperty;
            if (!property.TryGetBackingFieldAndSpecifiedProperty(codeType, out backingField, out specifiedProperty))
                continue;
            var specifiedField = specifiedProperty.GetBackingField(codeType);
            if (specifiedField == null)
                continue;
            toRemove.Add(specifiedProperty);
            toRemove.Add(specifiedField);

            if (promoteToNullable)
            
                // Do not do this for attributes
                if (property.CustomAttributes.Cast<CodeAttributeDeclaration>().Any(a => a.AttributeType.BaseType == typeof(System.Xml.Serialization.XmlAttributeAttribute).FullName))
                    continue;
                var typeRef = property.Type;
                if (typeRef.ArrayRank > 0)
                    // An array - not a reference type.
                    continue;

                // OK, two possibilities here:
                // 1) The property might reference some system type such as DateTime or decimal
                // 2) The property might reference some type being defined such as an enum or struct.

                var type = Type.GetType(typeRef.BaseType);
                if (type != null)
                
                    if (!type.IsClass)
                    
                        if (type == typeof(Nullable<>))
                            // Already nullable
                            continue;
                        else if (!type.IsGenericTypeDefinition && (type.IsValueType || type.IsEnum) && Nullable.GetUnderlyingType(type) == null)
                        
                            var nullableType = typeof(Nullable<>).MakeGenericType(type);
                            var newRefType = new CodeTypeReference(nullableType);
                            property.Type = newRefType;
                            backingField.Type = newRefType;
                        
                    
                
                else
                
                    var generatedType = codeNamespace.FindCodeType(typeRef);
                    if (generatedType != null)
                    
                        if (generatedType.IsStruct || generatedType.IsEnum)
                        
                            var newRefType = new CodeTypeReference(typeof(Nullable<>).FullName, typeRef);
                            property.Type = newRefType;
                            backingField.Type = newRefType;
                        
                    
                
            
        
        foreach (var member in toRemove)
        
            codeType.Members.Remove(member);
        
    


public static class CodeNamespaceExtensions

    public static CodeTypeDeclaration FindCodeType(this CodeNamespace codeNamespace, CodeTypeReference reference)
    
        if (codeNamespace == null)
            throw new ArgumentNullException();
        if (reference == null)
            return null;
        CodeTypeDeclaration foundType = null;
        foreach (CodeTypeDeclaration codeType in codeNamespace.Types)
        
            if (codeType.Name == reference.BaseType)
            
                if (foundType == null)
                    foundType = codeType;
                else if (foundType != codeType)
                
                    foundType = null;
                    break;
                
            
        
        return foundType;
    


public static class CodeMemberPropertyExtensions

    public static bool TryGetBackingFieldAndSpecifiedProperty(this CodeMemberProperty property, CodeTypeDeclaration codeType,
        out CodeMemberField backingField, out CodeMemberProperty specifiedProperty)
    
        if (property == null)
        
            backingField = null;
            specifiedProperty = null;
            return false;
        

        if ((backingField = property.GetBackingField(codeType)) == null)
        
            specifiedProperty = null;
            return false;
        

        specifiedProperty = null;
        var specifiedName = property.Name + "Specified";
        foreach (var p in codeType.Members.OfType<CodeMemberProperty>())
        
            if (p.Name == specifiedName)
            
                // Make sure the property is marked as XmlIgnore (there might be a legitimate, serializable property
                // named xxxSpecified).
                if (!p.CustomAttributes.Cast<CodeAttributeDeclaration>().Any(a => a.AttributeType.BaseType == typeof(System.Xml.Serialization.XmlIgnoreAttribute).FullName))
                    continue;
                if (specifiedProperty == null)
                    specifiedProperty = p;
                else if (specifiedProperty != p)
                
                    specifiedProperty = null;
                    break;
                
            
        
        if (specifiedProperty == null)
            return false;
        if (specifiedProperty.GetBackingField(codeType) == null)
            return false;
        return true;
    

    public static CodeMemberField GetBackingField(this CodeMemberProperty property, CodeTypeDeclaration codeType)
    
        if (property == null)
            return null;

        CodeMemberField returnedField = null;
        foreach (var statement in property.GetStatements.OfType<CodeMethodReturnStatement>())
        
            var expression = statement.Expression as CodeFieldReferenceExpression;
            if (expression == null)
                return null;
            if (!(expression.TargetObject is CodeThisReferenceExpression))
                return null;
            var fieldName = expression.FieldName;
            foreach (var field in codeType.Members.OfType<CodeMemberField>())
            
                if (field.Name == fieldName)
                
                    if (returnedField == null)
                        returnedField = field;
                    else if (returnedField != field)
                        return null;
                
            
        

        return returnedField;
    


public abstract class CustomXsdCodeGeneratorBase

    // This base class adapted from http://mikehadlow.blogspot.com/2007/01/writing-your-own-xsdexe.html

    readonly string Namespace;

    public CustomXsdCodeGeneratorBase(string Namespace)
    
        this.Namespace = Namespace;
    

    public void XsdToClassTest(IEnumerable<string> xsds, TextWriter codeWriter)
    
        XsdToClassTest(xsds.Select(xsd => (Func<TextReader>)(() => new StringReader(xsd))), codeWriter);
    

    public void XsdToClassTest(IEnumerable<Func<TextReader>> xsds, TextWriter codeWriter)
    
        var schemas = new XmlSchemas();

        foreach (var getReader in xsds)
        
            using (var reader = getReader())
            
                var xsd = XmlSchema.Read(reader, null);
                schemas.Add(xsd);
            
        

        schemas.Compile(null, true);
        var schemaImporter = new XmlSchemaImporter(schemas);

        var maps = new List<XmlTypeMapping>();
        foreach (XmlSchema xsd in schemas)
        
            foreach (XmlSchemaType schemaType in xsd.SchemaTypes.Values)
            
                maps.Add(schemaImporter.ImportSchemaType(schemaType.QualifiedName));
            
            foreach (XmlSchemaElement schemaElement in xsd.Elements.Values)
            
                maps.Add(schemaImporter.ImportTypeMapping(schemaElement.QualifiedName));
            
        

        // create the codedom
        var codeNamespace = new CodeNamespace(this.Namespace);
        var codeExporter = new XmlCodeExporter(codeNamespace);
        foreach (XmlTypeMapping map in maps)
        
            codeExporter.ExportTypeMapping(map);
        

        ModifyGeneratedNamespace(codeNamespace);

        // Check for invalid characters in identifiers
        CodeGenerator.ValidateIdentifiers(codeNamespace);

        // output the C# code
        var codeProvider = new CSharpCodeProvider();
        codeProvider.GenerateCodeFromNamespace(codeNamespace, codeWriter, new CodeGeneratorOptions());
    

    protected virtual void ModifyGeneratedNamespace(CodeNamespace codeNamespace)
    
        foreach (CodeTypeDeclaration codeType in codeNamespace.Types)
        
            ModifyGeneratedCodeTypeDeclaration(codeType, codeNamespace);
        
    

    protected virtual void ModifyGeneratedCodeTypeDeclaration(CodeTypeDeclaration codeType, CodeNamespace codeNamespace)
    
    

为了测试它,我创建了以下类型:

namespace SampleClasses

    public class SimleSampleClass
    
        [XmlElement]
        public decimal Something  get; set; 

        [XmlIgnore]
        public bool SomethingSpecified  get; set; 
    

    [XmlRoot("RootClass")]
    public class RootClass
    
        [XmlArray]
        [XmlArrayItem("SampleClass")]
        public List<SampleClass> SampleClasses  get; set; 
    

    [XmlRoot("SampleClass")]
    public class SampleClass
    
        [XmlAttribute]
        public long Id  get; set; 

        public decimal Something  get; set; 

        [XmlIgnore]
        public bool SomethingSpecified  get; set; 

        public SomeEnum SomeEnum  get; set; 

        [XmlIgnore]
        public bool SomeEnumSpecified  get; set; 

        public string SomeString  get; set; 

        [XmlIgnore]
        public bool SomeStringSpecified  get; set; 

        public decimal? SomeNullable  get; set; 

        [XmlIgnore]
        public bool SomeNullableSpecified  get; set; 

        public DateTime SomeDateTime  get; set; 

        [XmlIgnore]
        public bool SomeDateTimeSpecified  get; set; 

        // https://***.com/questions/3280362/most-elegant-xml-serialization-of-color-structure

        [XmlElement(Type = typeof(XmlColor))]
        public Color MyColor  get; set; 

        [XmlIgnore]
        public bool MyColorSpecified  get; set; 
    

    public enum SomeEnum
    
        DefaultValue,
        FirstValue,
        SecondValue,
        ThirdValue,
    

    // https://***.com/questions/3280362/most-elegant-xml-serialization-of-color-structure
    public struct XmlColor
    
        private Color? color_;

        private Color Color
        
            get
            
                return color_ ?? Color.Black;
            
            set
            
                color_ = value;
            
        

        public XmlColor(Color c)  color_ = c; 

        public Color ToColor()
        
            return Color;
        

        public void FromColor(Color c)
        
            Color = c;
        

        public static implicit operator Color(XmlColor x)
        
            return x.ToColor();
        

        public static implicit operator XmlColor(Color c)
        
            return new XmlColor(c);
        

        [XmlAttribute]
        public string Web
        
            get  return ColorTranslator.ToHtml(Color); 
            set
            
                try
                
                    if (Alpha == 0xFF) // preserve named color value if possible
                        Color = ColorTranslator.FromHtml(value);
                    else
                        Color = Color.FromArgb(Alpha, ColorTranslator.FromHtml(value));
                
                catch (Exception)
                
                    Color = Color.Black;
                
            
        

        [XmlAttribute]
        public byte Alpha
        
            get  return Color.A; 
            set
            
                if (value != Color.A) // avoid hammering named color if no alpha change
                    Color = Color.FromArgb(value, Color);
            
        

        public bool ShouldSerializeAlpha()  return Alpha < 0xFF; 
    

使用通用 xsd.exe 我从它们生成了以下架构:

<xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="SimleSampleClass" nillable="true" type="SimleSampleClass" />
  <xs:complexType name="SimleSampleClass">
    <xs:sequence>
      <xs:element minOccurs="0" maxOccurs="1" name="Something" type="xs:decimal" />
    </xs:sequence>
  </xs:complexType>
  <xs:element name="RootClass" nillable="true" type="RootClass" />
  <xs:complexType name="RootClass">
    <xs:sequence>
      <xs:element minOccurs="0" maxOccurs="1" name="SampleClasses" type="ArrayOfSampleClass" />
    </xs:sequence>
  </xs:complexType>
  <xs:complexType name="ArrayOfSampleClass">
    <xs:sequence>
      <xs:element minOccurs="0" maxOccurs="unbounded" name="SampleClass" nillable="true" type="SampleClass" />
    </xs:sequence>
  </xs:complexType>
  <xs:complexType name="SampleClass">
    <xs:sequence>
      <xs:element minOccurs="0" maxOccurs="1" name="Something" type="xs:decimal" />
      <xs:element minOccurs="0" maxOccurs="1" name="SomeEnum" type="SomeEnum" />
      <xs:element minOccurs="0" maxOccurs="1" name="SomeString" type="xs:string" />
      <xs:element minOccurs="0" maxOccurs="1" name="SomeNullable" nillable="true" type="xs:decimal" />
      <xs:element minOccurs="0" maxOccurs="1" name="SomeDateTime" type="xs:dateTime" />
      <xs:element minOccurs="0" maxOccurs="1" name="MyColor" type="XmlColor" />
    </xs:sequence>
    <xs:attribute name="Id" type="xs:long" use="required" />
  </xs:complexType>
  <xs:simpleType name="SomeEnum">
    <xs:restriction base="xs:string">
      <xs:enumeration value="DefaultValue" />
      <xs:enumeration value="FirstValue" />
      <xs:enumeration value="SecondValue" />
      <xs:enumeration value="ThirdValue" />
    </xs:restriction>
  </xs:simpleType>
  <xs:complexType name="XmlColor">
    <xs:attribute name="Web" type="xs:string" />
    <xs:attribute name="Alpha" type="xs:unsignedByte" />
  </xs:complexType>
  <xs:element name="SampleClass" nillable="true" type="SampleClass" />
  <xs:element name="SomeEnum" type="SomeEnum" />
  <xs:element name="XmlColor" type="XmlColor" />
</xs:schema>

并且,使用此架构,我使用 CustomXsdCodeGeneratorpromoteToNullable = trueNamespace = "Question42295155" 重新生成了以下 c# 类:

namespace Question42295155 


    /// <remarks/>
    [System.CodeDom.Compiler.GeneratedCodeAttribute("XsdToClassTest", "1.0.0.0")]
    [System.SerializableAttribute()]
    [System.Diagnostics.DebuggerStepThroughAttribute()]
    [System.ComponentModel.DesignerCategoryAttribute("code")]
    [System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=true)]
    public partial class SimleSampleClass 

        private System.Nullable<decimal> somethingField;

        /// <remarks/>
        public System.Nullable<decimal> Something 
            get 
                return this.somethingField;
            
            set 
                this.somethingField = value;
            
        
    

    /// <remarks/>
    [System.CodeDom.Compiler.GeneratedCodeAttribute("XsdToClassTest", "1.0.0.0")]
    [System.SerializableAttribute()]
    [System.Diagnostics.DebuggerStepThroughAttribute()]
    [System.ComponentModel.DesignerCategoryAttribute("code")]
    [System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=true)]
    public partial class SampleClass 

        private System.Nullable<decimal> somethingField;

        private System.Nullable<SomeEnum> someEnumField;

        private string someStringField;

        private System.Nullable<decimal> someNullableField;

        private System.Nullable<System.DateTime> someDateTimeField;

        private XmlColor myColorField;

        private long idField;

        /// <remarks/>
        public System.Nullable<decimal> Something 
            get 
                return this.somethingField;
            
            set 
                this.somethingField = value;
            
        

        /// <remarks/>
        public System.Nullable<SomeEnum> SomeEnum 
            get 
                return this.someEnumField;
            
            set 
                this.someEnumField = value;
            
        

        /// <remarks/>
        public string SomeString 
            get 
                return this.someStringField;
            
            set 
                this.someStringField = value;
            
        

        /// <remarks/>
        [System.Xml.Serialization.XmlElementAttribute(IsNullable=true)]
        public System.Nullable<decimal> SomeNullable 
            get 
                return this.someNullableField;
            
            set 
                this.someNullableField = value;
            
        

        /// <remarks/>
        public System.Nullable<System.DateTime> SomeDateTime 
            get 
                return this.someDateTimeField;
            
            set 
                this.someDateTimeField = value;
            
        

        /// <remarks/>
        public XmlColor MyColor 
            get 
                return this.myColorField;
            
            set 
                this.myColorField = value;
            
        

        /// <remarks/>
        [System.Xml.Serialization.XmlAttributeAttribute()]
        public long Id 
            get 
                return this.idField;
            
            set 
                this.idField = value;
            
        
    

    /// <remarks/>
    [System.CodeDom.Compiler.GeneratedCodeAttribute("XsdToClassTest", "1.0.0.0")]
    [System.SerializableAttribute()]
    [System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=false)]
    public enum SomeEnum 

        /// <remarks/>
        DefaultValue,

        /// <remarks/>
        FirstValue,

        /// <remarks/>
        SecondValue,

        /// <remarks/>
        ThirdValue,
    

    /// <remarks/>
    [System.CodeDom.Compiler.GeneratedCodeAttribute("XsdToClassTest", "1.0.0.0")]
    [System.SerializableAttribute()]
    [System.Diagnostics.DebuggerStepThroughAttribute()]
    [System.ComponentModel.DesignerCategoryAttribute("code")]
    [System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=true)]
    public partial class XmlColor 

        private string webField;

        private byte alphaField;

        /// <remarks/>
        [System.Xml.Serialization.XmlAttributeAttribute()]
        public string Web 
            get 
                return this.webField;
            
            set 
                this.webField = value;
            
        

        /// <remarks/>
        [System.Xml.Serialization.XmlAttributeAttribute()]
        public byte Alpha 
            get 
                return this.alphaField;
            
            set 
                this.alphaField = value;
            
        
    

    /// <remarks/>
    [System.CodeDom.Compiler.GeneratedCodeAttribute("XsdToClassTest", "1.0.0.0")]
    [System.SerializableAttribute()]
    [System.Diagnostics.DebuggerStepThroughAttribute()]
    [System.ComponentModel.DesignerCategoryAttribute("code")]
    [System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=true)]
    public partial class RootClass 

        private SampleClass[] sampleClassesField;

        /// <remarks/>
        public SampleClass[] SampleClasses 
            get 
                return this.sampleClassesField;
            
            set 
                this.sampleClassesField = value;
            
        
    

    /// <remarks/>
    [System.CodeDom.Compiler.GeneratedCodeAttribute("XsdToClassTest", "1.0.0.0")]
    [System.SerializableAttribute()]
    [System.Diagnostics.DebuggerStepThroughAttribute()]
    [System.ComponentModel.DesignerCategoryAttribute("code")]
    [System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=true)]
    public partial class ArrayOfSampleClass 

        private SampleClass[] sampleClassField;

        /// <remarks/>
        [System.Xml.Serialization.XmlElementAttribute("SampleClass", IsNullable=true)]
        public SampleClass[] SampleClass 
            get 
                return this.sampleClassField;
            
            set 
                this.sampleClassField = value;
            
        
    

注意:

没有以Specified 结尾的属性。

属性SomethingSomeEnumSomeDateTime 已变为可为空。

已经可以为空的public decimal? SomeNullable get; set; 往返于public System.Nullable&lt;decimal&gt; SomeNullable,而不是因为变成一些可怕的双空System.Nullable&lt;System.Nullable&lt;decimal&gt;&gt; 而失败。

然后我从最初的 RootClass 生成了以下 XML:

<RootClass xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <SampleClasses>
    <SampleClass Id="10101">
      <Something>2.718</Something>
      <SomeEnum>ThirdValue</SomeEnum>
      <SomeString>hello</SomeString>
      <SomeNullable>3.14</SomeNullable>
      <SomeDateTime>2017-02-28T00:00:00-05:00</SomeDateTime>
      <MyColor Web="Maroon" />
    </SampleClass>
  </SampleClasses>
</RootClass>

并且能够成功地将其反序列化为生成的类Question42295155.RootClass,而不会丢失数据。

注意 - 此代码经过轻微测试。如果您愿意,我可以使用示例架构重新测试。

有关详细信息,请参阅Code Generation in the .NET Framework Using XML Schema。

【讨论】:

以上是关于XmlCodeExporter 和可为空的类型的主要内容,如果未能解决你的问题,请参考以下文章

XmlSerializer 和可为空的属性

JPA 可嵌入 PK 和可为空的字段

XmlConvert 和可为空的结果?

Laravel 验证中有时和可为空的区别

如何在基于块的 API 方法中使用非空和可为空的 Objective-C 关键字

C# 8.0 和可为空引用类型