Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

XmlCodeExporter and nullable types

System.Xml.Serialization.XmlCodeExporter generates code (in code CodeDom form) from an XSD schema. But it does it with some quirks. For example an optional element:

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

I would expect this to generate a corresponding code member of type Nullable<decimal>, but it actually creates a member of type decimal, and then a separate SomethingSpecified field which should be toggled separately to indicate a null value. This is probably because the library is from before the introduction of nullable types, but it leads to really inconvenient code.

Is it possible to adjust this code generation, or is there an alternative tool which generate better code in this case?

Edit: I know I can modify the schema and add nillable='true', but I don't want to change the schema to work around limitations of the code generation.

like image 880
JacquesB Avatar asked Feb 17 '17 10:02

JacquesB


1 Answers

The article Writing your own XSD.exe by Mike Hadlow gives a basic framework for creating your own version of xsd.exe. It has the following steps:

  1. Import the schemas with XmlSchema.Read() and XmlSchemaImporter.

  2. Generate .Net type(s) and attribute(s) to create using XmlCodeExporter.

  3. Tweak the generated types and attributes as desired.

    Here you would want to remove the generated xxxSpecified properties and promote their corresponding "real" properties to nullable.

  4. Generate the final code using CSharpCodeProvider.

Using this framework, and by experimentally determining what types are actually generated by XmlCodeExporter using a debugger, I created the following 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)
    {
    }
}

To test it, I created the following types:

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://stackoverflow.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://stackoverflow.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; }
    }
}

Using the generic xsd.exe I generated the following schema from them:

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

And, using this schema, I regenerated the following c# classes using CustomXsdCodeGenerator with promoteToNullable = true and Namespace = "Question42295155":

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

Notice that:

  • There are no properties ending in the name Specified.

  • The properties Something, SomeEnum and SomeDateTime have become nullable.

  • The already-nullable public decimal? SomeNullable { get; set; } round-trips to public System.Nullable<decimal> SomeNullable rather than failing by becoming some dreadful double-nullable System.Nullable<System.Nullable<decimal>>.

I then generated the following XML from the initial RootClass:

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

And was able to successfully deserialize it to the generated class Question42295155.RootClass without data loss.

Note - this code is lightly tested. I can re-test with a sample schema if you would like to provide one.

For further information, see Code Generation in the .NET Framework Using XML Schema.

like image 122
dbc Avatar answered Nov 03 '22 08:11

dbc