XSD Tools in .NET8 – Part5 – XmlSchemaClassGenerator - Simple

Mark Pelf - Nov 8 - - Dev Community

A practical guide to XSD tools available in .NET8 environment.

Abstract: A practical guide to XML and XSD tools available in .NET8 environment, focusing on generating and using C# classes to process some XML valid for some given XSD (technology as of September 2024).

1 Doing XML and XSD related work in .NET8

I was recently doing some work related to XML and XSD processing in .NET8 environment and created several proof-of-concept applications to evaluate the tools available. These articles are the result of my prototyping work.

1.1 List of tools used/tested

Here are the tools used/tested:

  • Visual Studio 2022
  • XSD.EXE (Microsoft license, part of VS2022)
  • XmlSchemaClassGenerator (Open Source/Freeware)
  • LinqToXsdCore (Open Source/Freeware)
  • Liquid XML Objects (Commercial license)

1.2 Articles in this series

For technical reasons, I will organize this text into several articles:

  • XSD Tools in .NET8 – Part1 – VS2022
  • XSD Tools in .NET8 – Part2 – C# validation
  • XSD Tools in .NET8 – Part3 – XsdExe – Simple
  • XSD Tools in .NET8 – Part4 – XsdExe - Advanced
  • XSD Tools in .NET8 – Part5 – XmlSchemaClassGenerator – Simple
  • XSD Tools in .NET8 – Part6 – XmlSchemaClassGenerator – Advanced
  • XSD Tools in .NET8 – Part7 – LinqToXsdCore – Simple
  • XSD Tools in .NET8 – Part8 – LinqToXsdCore – Advanced
  • XSD Tools in .NET8 – Part9 – LiquidXMLObjects – Simple
  • XSD Tools in .NET8 – Part10 – LiquidXMLObjects – Advanced

2 More theory about XML and XSD rules

Here is some more theory about XML and XSD rules.

2.1 Optional Xml-Element and Xml-Attribute

Optional: Does not need to be present in the XML.

For XSD Schema elements:

Optional: minOccurs="0" attribute ->In order to set a schema element as optional, you include the minOccurs="0" attribute

3 Examples of XML and XSD

Here are some sample XML-s and XSD-s I created for test purposes.

3.1 Simple case

Please note that this example XML/XSD has an Optional Xml-Element. Read the comments inside for more details.

<?xml version="1.0" encoding="utf-8"?>
<!--SmallCompany.xsd++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-->
<xs:schema attributeFormDefault="unqualified" 
           elementFormDefault="qualified" 
           targetNamespace="https://markpelf.com/SmallCompany.xsd" 
           xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="SmallCompany">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="CompanyName" type="xs:string" />
                <xs:element maxOccurs="unbounded" name="Employee">
                    <xs:complexType>
                        <xs:sequence>
                            <!--Name_String_NO is String NotOptional-->
                            <xs:element name="Name_String_NO" type="xs:string" />
                            <!--City_String_O is String Optional-->
                            <xs:element minOccurs="0" name="City_String_O" type="xs:string" />
                        </xs:sequence>
                    </xs:complexType>
                </xs:element>
                <xs:element maxOccurs="unbounded" name="InfoData">
                    <xs:complexType>
                        <xs:sequence>
                            <!--Id_Int_NO is Int NotOptional-->
                            <xs:element name="Id_Int_NO" type="xs:int" />
                            <!--Quantity_Int_O is Int Optional-->
                            <xs:element minOccurs="0" name="Quantity_Int_O" type="xs:int" />
                        </xs:sequence>
                    </xs:complexType>
                </xs:element>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
</xs:schema>
Enter fullscreen mode Exit fullscreen mode
<?xml version="1.0" encoding="utf-8"?>
<!--SmallCompany.xsd++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-->
<xs:schema attributeFormDefault="unqualified" 
           elementFormDefault="qualified" 
           targetNamespace="https://markpelf.com/SmallCompany.xsd" 
           xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="SmallCompany">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="CompanyName" type="xs:string" />
                <xs:element maxOccurs="unbounded" name="Employee">
                    <xs:complexType>
                        <xs:sequence>
                            <!--Name_String_NO is String NotOptional-->
                            <xs:element name="Name_String_NO" type="xs:string" />
                            <!--City_String_O is String Optional-->
                            <xs:element minOccurs="0" name="City_String_O" type="xs:string" />
                        </xs:sequence>
                    </xs:complexType>
                </xs:element>
                <xs:element maxOccurs="unbounded" name="InfoData">
                    <xs:complexType>
                        <xs:sequence>
                            <!--Id_Int_NO is Int NotOptional-->
                            <xs:element name="Id_Int_NO" type="xs:int" />
                            <!--Quantity_Int_O is Int Optional-->
                            <xs:element minOccurs="0" name="Quantity_Int_O" type="xs:int" />
                        </xs:sequence>
                    </xs:complexType>
                </xs:element>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
</xs:schema>
Enter fullscreen mode Exit fullscreen mode

4 Using XmlSchemaClassGenerator tool to create C# class

We focus in this article on the usage of XmlSchemaClassGenerator tool to generate C# class from XSD file.

Here is the tool's basic info.


Tool name============================
XmlSchemaClassGenerator 

License============================ 
Open Source/Freeware

Where to get it============================
https://github.com/mganss/XmlSchemaClassGenerator?tab=readme-ov-file

Version============================
Windows PowerShell > ./XmlSchemaClassGenerator.Console.exe
Usage: xscgen [OPTIONS]+ xsdFile...
Generate C# classes from XML Schema files.
Version 2.1.1162.0

Help============================
Windows PowerShell > ./XmlSchemaClassGenerator.Console.exe
Usage: xscgen [OPTIONS]+ xsdFile...
Generate C# classes from XML Schema files.
Version 2.1.1162.0
xsdFiles may contain globs, e.g. "content\{schema,xsd}\**\*.xsd", and URLs.
Append - to option to disable it, e.g. --interface-.

Options:
  -h, --help                 show this message and exit
  -n, --namespace=VALUE      map an XML namespace to a C# namespace
                               Separate XML namespace and C# namespace by '='.
                               A single value (no '=') is taken as the C#
                               namespace the empty XML namespace is mapped to.
                               One option must be given for each namespace to
                               be mapped.
                               A file name may be given by appending a pipe
                               sign (|) followed by a file name (like schema.
                               xsd) to the XML namespace.
                               If no mapping is found for an XML namespace, a
                               name is generated automatically (may fail).
      --nf, --namespaceFile=VALUE
                             file containing mappings from XML namespaces to C#
                               namespaces
                               The line format is one mapping per line: XML
                               namespace = C# namespace [optional file name].
                               Lines starting with # and empty lines are
                               ignored.
      --tns, --typeNameSubstitute=VALUE
                             substitute a generated type/member name
                               Separate type/member name and substitute name by
                               '='.
                               Prefix type/member name with an appropriate kind
                               ID as documented at: https://t.ly/HHEI.
                               Prefix with 'A:' to substitute any type/member.
      --tnsf, --typeNameSubstituteFile=VALUE
                             file containing generated type/member name
                               substitute mappings
                               The line format is one mapping per line:
                               prefixed type/member name = substitute name.
                               Lines starting with # and empty lines are
                               ignored.
  -o, --output=FOLDER        the FOLDER to write the resulting .cs files to
  -d, --datetime-offset      map xs:datetime and derived types to System.
                               DateTimeOffset instead of System.DateTime
  -i, --integer=TYPE         map xs:integer and derived types to TYPE instead
                               of automatic approximation
                               TYPE can be i[nt], l[ong], or d[ecimal]
      --fb, --fallback, --use-integer-type-as-fallback
                             use integer type specified via -i only if no type
                               can be deduced
  -e, --edb, --enable-data-binding
                             enable INotifyPropertyChanged data binding
  -r, --order                emit order for all class members stored as XML
                               element
  -c, --pcl                  PCL compatible output
  -p, --prefix=PREFIX        the PREFIX to prepend to auto-generated namespace
                               names
  -v, --verbose              print generated file names on stdout
  -0, --nullable             generate nullable adapter properties for optional
                               elements/attributes w/o default values
  -f, --ef                   generate Entity Framework Code First compatible
                               classes
  -t, --interface            generate interfaces for groups and attribute
                               groups (default is enabled)
  -a, --pascal               use Pascal case for class and property names (
                               default is enabled)
      --av, --assemblyVisible
                             use the internal visibility modifier (default is
                               false)
  -u, --enableUpaCheck       should XmlSchemaSet check for Unique Particle
                               Attribution (UPA) (default is enabled)
      --ct, --collectionType=VALUE
                             collection type to use (default is System.
                               Collections.ObjectModel.Collection`1)
      --cit, --collectionImplementationType=VALUE
                             the default collection type implementation to use (
                               default is null)
      --csm, --collectionSettersMode=Private, Public, PublicWithoutConstructorInitialization, Init, InitWithoutConstructorInitialization
                             generate a private, public, or init-only setter
                               with or without backing field initialization for
                               collections
                               (default is Private; can be: Private, Public,
                               PublicWithoutConstructorInitialization, Init,
                               InitWithoutConstructorInitialization)
      --ctro, --codeTypeReferenceOptions=GlobalReference, GenericTypeParameter
                             the default CodeTypeReferenceOptions Flags to use (
                               default is unset; can be: GlobalReference,
                               GenericTypeParameter)
      --tvpn, --textValuePropertyName=VALUE
                             the name of the property that holds the text value
                               of an element (default is Value)
      --dst, --debuggerStepThrough
                             generate DebuggerStepThroughAttribute (default is
                               enabled)
      --dc, --disableComments
                             do not include comments from xsd
      --nu, --noUnderscore   do not generate underscore in private member name (
                               default is false)
      --da, --description    generate DescriptionAttribute (default is true)
      --cc, --complexTypesForCollections
                             generate complex types for collections (default is
                               true)
  -s, --useShouldSerialize   use ShouldSerialize pattern instead of Specified
                               pattern (default is false)
      --sf, --separateFiles  generate a separate file for each class (default
                               is false)
      --nh, --namespaceHierarchy
                             generate a separate folder for namespace hierarchy.
                                Implies "separateFiles" if true (default is
                               false)
      --sg, --separateSubstitutes
                             generate a separate property for each element of a
                               substitution group (default is false)
      --dnfin, --doNotForceIsNullable
                             do not force generator to emit IsNullable = true
                               in XmlElement annotation for nillable elements
                               when element is nullable (minOccurs < 1 or
                               parent element is choice) (default is false)
      --cn, --compactTypeNames
                             use type names without namespace qualifier for
                               types in the using list (default is false)
      --cl, --commentLanguages=VALUE
                             comment languages to use (default is en; supported
                               are en, de)
      --un, --uniqueTypeNames
                             generate type names that are unique across
                               namespaces (default is false)
      --gc, --generatedCodeAttribute
                             add version information to GeneratedCodeAttribute (
                               default is true)
      --nc, --netCore        generate .NET Core specific code that might not
                               work with .NET Framework (default is false)
      --nr, --nullableReferenceAttributes
                             generate attributes for nullable reference types (
                               default is false)
      --ar, --useArrayItemAttribute
                             use ArrayItemAttribute for sequences with single
                               elements (default is true)
      --es, --enumAsString   Use string instead of enum for enumeration
      --dmb, --disableMergeRestrictionsWithBase
                             Disable merging of simple type restrictions with
                               base type restrictions
      --ca, --commandArgs    generate a comment with the exact command line
                               arguments that were used to generate the source
                               code (default is true)
      --uc, --unionCommonType
                             generate a common type for unions if possible (
                               default is false)
      --ec, --serializeEmptyCollections
                             serialize empty collections (default is false)
      --dtd, --allowDtdParse allows dtd parse (default is false)
      --ns, --namingScheme=VALUE
                             use the specified naming scheme for class and
                               property names (default is Pascal; can be:
                               Direct, Pascal, Legacy)

=================================================   
Usage Examples===================                         
Instructions to generate C# class 
Windows PowerShell> ./XmlSchemaClassGenerator.Console.exe --namespace=Example2SmallCompany --nullable --netCore --nullableReferenceAttributes --namingScheme=Direct SmallCompany.xsd
Windows PowerShell>  ./XmlSchemaClassGenerator.Console.exe --namespace=Example2BigCompany --nullable --netCore --nullableReferenceAttributes --namingScheme=Direct BigCompany.xsd

============================
Enter fullscreen mode Exit fullscreen mode

5 Generated C# class

Here is the C# generated by the above tool based on the above presented XSD SmallCompany.xsd.

Here is the class's full code.

//SmallCompany_ver2_2.cs
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.42000
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

// This code was generated by XmlSchemaClassGenerator version 2.1.1162.0 using the following command:
// XmlSchemaClassGenerator.Console --namespace=Example2SmallCompany --nullable --netCore --nullableReferenceAttributes --namingScheme=Direct SmallCompany.xsd
namespace Example2SmallCompany
{


    [System.CodeDom.Compiler.GeneratedCodeAttribute("XmlSchemaClassGenerator", "2.1.1162.0")]
    [System.SerializableAttribute()]
    [System.Xml.Serialization.XmlTypeAttribute("SmallCompany", Namespace="https://markpelf.com/SmallCompany.xsd", AnonymousType=true)]
    [System.Diagnostics.DebuggerStepThroughAttribute()]
    [System.ComponentModel.DesignerCategoryAttribute("code")]
    [System.Xml.Serialization.XmlRootAttribute("SmallCompany", Namespace="https://markpelf.com/SmallCompany.xsd")]
    public partial class SmallCompany
    {

        [System.ComponentModel.DataAnnotations.RequiredAttribute(AllowEmptyStrings=true)]
        [System.Xml.Serialization.XmlElementAttribute("CompanyName")]
        public string CompanyName { get; set; }

        [System.Xml.Serialization.XmlIgnoreAttribute()]
        private System.Collections.ObjectModel.Collection<SmallCompanyEmployee> _employee;

        [System.ComponentModel.DataAnnotations.RequiredAttribute(AllowEmptyStrings=true)]
        [System.Xml.Serialization.XmlElementAttribute("Employee")]
        public System.Collections.ObjectModel.Collection<SmallCompanyEmployee> Employee
        {
            get
            {
                return _employee;
            }
            private set
            {
                _employee = value;
            }
        }

        /// <summary>
        /// <para xml:lang="en">Initializes a new instance of the <see cref="SmallCompany" /> class.</para>
        /// </summary>
        public SmallCompany()
        {
            this._employee = new System.Collections.ObjectModel.Collection<SmallCompanyEmployee>();
            this._infoData = new System.Collections.ObjectModel.Collection<SmallCompanyInfoData>();
        }

        [System.Xml.Serialization.XmlIgnoreAttribute()]
        private System.Collections.ObjectModel.Collection<SmallCompanyInfoData> _infoData;

        [System.ComponentModel.DataAnnotations.RequiredAttribute(AllowEmptyStrings=true)]
        [System.Xml.Serialization.XmlElementAttribute("InfoData")]
        public System.Collections.ObjectModel.Collection<SmallCompanyInfoData> InfoData
        {
            get
            {
                return _infoData;
            }
            private set
            {
                _infoData = value;
            }
        }
    }

    [System.CodeDom.Compiler.GeneratedCodeAttribute("XmlSchemaClassGenerator", "2.1.1162.0")]
    [System.SerializableAttribute()]
    [System.Xml.Serialization.XmlTypeAttribute("SmallCompanyEmployee", Namespace="https://markpelf.com/SmallCompany.xsd", AnonymousType=true)]
    [System.Diagnostics.DebuggerStepThroughAttribute()]
    [System.ComponentModel.DesignerCategoryAttribute("code")]
    public partial class SmallCompanyEmployee
    {

        [System.ComponentModel.DataAnnotations.RequiredAttribute(AllowEmptyStrings=true)]
        [System.Xml.Serialization.XmlElementAttribute("Name_String_NO")]
        public string Name_String_NO { get; set; }

        [System.Diagnostics.CodeAnalysis.AllowNullAttribute()]
        [System.Diagnostics.CodeAnalysis.MaybeNullAttribute()]
        [System.Xml.Serialization.XmlElementAttribute("City_String_O")]
        public string City_String_O { get; set; }
    }

    [System.CodeDom.Compiler.GeneratedCodeAttribute("XmlSchemaClassGenerator", "2.1.1162.0")]
    [System.SerializableAttribute()]
    [System.Xml.Serialization.XmlTypeAttribute("SmallCompanyInfoData", Namespace="https://markpelf.com/SmallCompany.xsd", AnonymousType=true)]
    [System.Diagnostics.DebuggerStepThroughAttribute()]
    [System.ComponentModel.DesignerCategoryAttribute("code")]
    public partial class SmallCompanyInfoData
    {

        [System.ComponentModel.DataAnnotations.RequiredAttribute(AllowEmptyStrings=true)]
        [System.Xml.Serialization.XmlElementAttribute("Id_Int_NO")]
        public int Id_Int_NO { get; set; }

        [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
        [System.Xml.Serialization.XmlElementAttribute("Quantity_Int_O")]
        public int Quantity_Int_OValue { get; set; }

        /// <summary>
        /// <para xml:lang="en">Gets or sets a value indicating whether the Quantity_Int_O property is specified.</para>
        /// </summary>
        [System.Xml.Serialization.XmlIgnoreAttribute()]
        [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
        public bool Quantity_Int_OValueSpecified { get; set; }

        [System.Xml.Serialization.XmlIgnoreAttribute()]
        public System.Nullable<int> Quantity_Int_O
        {
            get
            {
                if (this.Quantity_Int_OValueSpecified)
                {
                    return this.Quantity_Int_OValue;
                }
                else
                {
                    return null;
                }
            }
            set
            {
                this.Quantity_Int_OValue = value.GetValueOrDefault();
                this.Quantity_Int_OValueSpecified = value.HasValue;
            }
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Here is the class diagram.

6 Two C# API styles for Optional Xml-Elements

There are two approaches/styles for marking Optional Xml-Element presence in generated C# code:

  1. The first is bool_flag_style – using a bool flag to indicate the presence of optional Xml-Element, with flag=false to indicate the Xml-Element was not present. For example, for some Xml-Element ElemA that, if present, will have value integer, you will get in C# generated two variables “bool ElemA_flag, int ElemA_value”. You need to check if element ElemA was present by first checking the flag ElemA_flag; and then if it is true, you go for the value of ElemA_value. If you do not check flag ElemA_flag first, and just go for the value of ElemA_value you might pick the default int value of zero (0), and you can not know if that is just the default value for C# variable that is always present, but Xml-Element was not present, or that element was present and it actually had the value of zero (0).
  2. The second is nullable_type_style – using a nullable type to indicate the presence of Xml-Element, with value=null to indicate the Xml-Element was not present. For example, for some Xml-Element ElemA that, if present, will have value integer, you will get in C# generated variable “int? ElemA_nullableValue”. You need to check if element ElemA was present by first checking the ElemA_nullableValue not being null; and then if it is not meaning the element was present, you go for the int value of ElemA_nullableValue.

7 Sample C# app

Here is a sample C# code using the above generated C# class to load and process the above presented XML SmallCompanyAAA.xml.

public static void ProcessVer2_Process1(
    string? filePath,
    Microsoft.Extensions.Logging.ILogger? logger)
{
    try
    {
        logger?.LogInformation(
            "+++ProcessVer2_Process1-Start++++++++++++++++++");
        logger?.LogInformation("filePath:" + filePath);

        XmlSerializer ser = new XmlSerializer(typeof(Example2SmallCompany.SmallCompany));
        TextReader textReader = File.OpenText(filePath ?? String.Empty);
        Example2SmallCompany.SmallCompany? xmlObject = 
            ser.Deserialize(textReader) as Example2SmallCompany.SmallCompany;

        if (xmlObject != null)
        {
            logger?.LogInformation("CompanyName:" + xmlObject.CompanyName);

            foreach(Example2SmallCompany.SmallCompanyEmployee item in xmlObject.Employee)
            {
                logger?.LogInformation("------------" );
                logger?.LogInformation("Name_String_NO:" + item.Name_String_NO);
                logger?.LogInformation("City_String_O:" + (item.City_String_O ?? "null") );
            }

            foreach (Example2SmallCompany.SmallCompanyInfoData item in xmlObject.InfoData)
            {
                logger?.LogInformation("------------");
                logger?.LogInformation("Id_Int_NO:" + item.Id_Int_NO.ToString());

                logger?.LogInformation("Quantity_Int_OValue:" + item.Quantity_Int_OValue.ToString());
                logger?.LogInformation("Quantity_Int_OValueSpecified:" + item.Quantity_Int_OValueSpecified.ToString());
                logger?.LogInformation("Quantity_Int_O:" + (item.Quantity_Int_O?.ToString() ?? "null"));
            }
        }
        else
        {
            logger?.LogError("xmlObject == null");
        }

        logger?.LogInformation(
            "+++ProcessVer2_Process1-End++++++++++++++++++");
    }
    catch (Exception ex)
    {
        string methodName =
            $"Type: {System.Reflection.MethodBase.GetCurrentMethod()?.DeclaringType?.FullName}, " +
            $"Method: ProcessVer2_Process1; ";
        logger?.LogError(ex, methodName);
    }
}

Enter fullscreen mode Exit fullscreen mode

And here is the log of execution.

 +++ProcessVer2_Process1-Start++++++++++++++++++
 filePath:C:\TmpXSD\XsdExample_Ver2\Example01\bin\Debug\net8.0\XmlFiles\SmallCompanyAAA.xml
 CompanyName:SmallCompanyAAA
 ------------
 Name_String_NO:Mark
 City_String_O:Belgrade
 ------------
 Name_String_NO:John
 City_String_O:null
 ------------
 Id_Int_NO:11
 Quantity_Int_OValue:123
 Quantity_Int_OValueSpecified:True
 Quantity_Int_O:123
 ------------
 Id_Int_NO:22
 Quantity_Int_OValue:0
 Quantity_Int_OValueSpecified:False
 Quantity_Int_O:null
 +++ProcessVer2_Process1-End++++++++++++++++++
Enter fullscreen mode Exit fullscreen mode

8 Analysis

It is not easy to understand what is happening here, but it looks like this tool is trying to use both “bool_flag_style” and “nullable_type_style” approaches/styles to mark Optional Xml-Element presence in generated C# code. The user can choose which API approach/style he or she likes.

  • Xml-Element Quantity_Int_O is presented by 3 C# variables: Quantity_Int_OValueSpecified – is a bool type (“bool_flag_style” API) Quantity_Int_OValue – is int type (“bool_flag_style” API) Quantity_Int_O – is int? type (“nullable_type_style” API)
    • Using “bool_flag_style” API: a) Quantity_Int_OValueSpecified – flag to indicate if the element was present or not b) Quantity_Int_OValue – if the above flag is true, then this is the element int value
    • Using “nullable_type_style” API: c) Quantity_Int_O - – is int? type and the meaning is
    • 1) null - means it was not present
    • 2) int – present and had value

9 Conclusion

This tool XmlSchemaClassGenerator is very interesting and available as a freeware. Code generated looked solid in my test. It can be of great interest to users that want to use nullable_type_style API, which is generally more modern approach to handling the Optional Xml-Elements.

The full example code project can be downloaded at GitHub [99].

10 References

[1] XML Schema

https://www.w3schools.com/xml/xml_schema.asp

[2] The Difference Between Optional and Not Required

https://www.infopathdev.com/blogs/greg/archive/2004/09/16/The-Difference-Between-Optional-and-Not-Required.aspx

[3] nillable and minOccurs XSD element attributes

https://stackoverflow.com/questions/1903062/nillable-and-minoccurs-xsd-element-attributes

[99] https://github.com/MarkPelf/XsdToolsInNet8

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .