XSD Tools in .NET8 – Part7 – LinqToXsdCore - Simple

Mark PelfMark Pelf
9 min read

XSD Tools in .NET8 – Part7 – LinqToXsdCore - Simple

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

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

4 Using LinqToXsdCore tool to create C# class

We focus in this article on the usage of LinqToXsdCore tool to generate C# class from XSD file.
Here is the tool's basic info.


Tool name:  ============================
XSD LinqToXsdCore

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

Where to get it============================
It is NuGet package of type DotnetTool. You need to install it in a special way.

Install Instructions====================================
Create somewhere directory Tool>
I had some additional custom sources. Just using --ignore-failed-sources wasn't enough. I went into Visual Studio -> Tools -> Options -> NuGet Package Manager -> Package Sources and unchecked the extra package sources so only nuget.org and Microsoft Visual Studio Offline Packages was left checked. After doing that I was able to install the tool , and then I re-enabled my custom package sources.

PowerShell, as Administrator

> dotnet new tool-manifest
> dotnet tool install LinqToXsdCore --ignore-failed-sources
You can invoke the tool from this directory using the following commands: 'dotnet tool run LinqToXsd' or 'dotnet LinqToXsd'.
Tool 'linqtoxsdcore' (version '3.4.7') was successfully installed. Entry is added to the manifest file _________\Tool\.config\dotnet-tools.json.


Version============================
Windows PowerShell > ./LinqToXsd.exe
LinqToXsd 3.4.7
Copyright (C) Muhammad Miftah, GitHub Contributors (2019-2022) & Microsoft Corporation (2008-2011)

Help============================
Windows PowerShell >  ./LinqToXsd.exe gen
LinqToXsd 3.4.7
Copyright (C) Muhammad Miftah, GitHub Contributors (2019-2022) & Microsoft Corporation (2008-2011)

ERROR(S):
  A required value not bound to option name is missing.

  -c, --Config                    (string) Specify the file or folder path to one or more configuration file(s).
                                  Multiple configuration files are merged as one. Incompatible with -AutoConfig

  -e, --EnableServiceReference    (bool) Imports the 'System.Xml.Serialization' namespace into the generated code.

  -a, --AutoConfig                (bool) Specify this with a folder containing XSDs and accompanying configuration
                                  files. This argument associate a configuration file with an XSD when one follows the
                                  naming convention: 'schema.xsd' + 'schema.xsd.config' - this is the default convention
                                  used by the 'config -e' verb when you specify a folder. Use this parameter to
                                  associate an XSD with its own configuration settings to prevent those settings being
                                  overriden or merged as the -Config argument would do. Only accepts folder paths.
                                  Incompatible with -Config. Will only generate code for XSDs that have an accompanying
                                  .config file. If no output is generated, run the 'config' verb on the folder first.

  -o, --Output                    (string) Output file name or folder. When specifying multiple input XSDs or input
                                  folders, and this value is a file, all output is merged into a single file. If this
                                  value is a folder, multiple output files are output to this folder.

  --help                          Display this help screen.

  --version                       Display version information.

  value pos. 1                    Required. (string[]) One or more schema files or folders containing schema files.
                                  Separate multiple files using a comma (,). If folders are given, then the files
                                  referenced in xs:include or xs:import directives are not imported twice. Usage:
                                  'LinqToXsd [verb] <file1.xsd>,<file2.xsd>' or 'LinqToXsd [verb] <folder1>,<folder2>'.
                                  You can also include folder and file paths in the same invocation: 'LinqToXsd [verb]
                                  <file1.xsd>,<folder1>'
====================================================    
Usage Examples===================                          
Instructions to generate C# class 
Windows PowerShell > dotnet LinqToXsd gen SmallCompany.xsd 
Windows PowerShell > dotnet LinqToXsd gen BigCompany.xsd 
====================================

5 Generated C# class

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

The class's full code produced by this tool is big (780 lines), so I will here show just the beginning of the file.

//SmallCompany_ver3_3.cs, 780 lines 
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace https.markpelf.com.SmallCompany.xsd {
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.IO;
    using System.Linq;
    using System.Diagnostics;
    using System.Xml;
    using System.Xml.Schema;
    using System.Xml.Linq;
    using Xml.Schema.Linq;


    /// <summary>
    /// <para>
    /// Regular expression: (CompanyName, Employee+, InfoData+)
    /// </para>
    /// </summary>
    public partial class SmallCompany : XTypedElement, IXMetaData {

        public void Save(string xmlFile) {
            XTypedServices.Save(xmlFile, Untyped);
        }

        public void Save(System.IO.TextWriter tw) {
            XTypedServices.Save(tw, Untyped);
        }

        public void Save(System.Xml.XmlWriter xmlWriter) {
            XTypedServices.Save(xmlWriter, Untyped);
        }

        public static SmallCompany Load(string xmlFile) {
            return XTypedServices.Load<SmallCompany>(xmlFile);
        }

        public static SmallCompany Load(System.IO.TextReader xmlFile) {
            return XTypedServices.Load<SmallCompany>(xmlFile);
        }

        public static SmallCompany Parse(string xml) {
            return XTypedServices.Parse<SmallCompany>(xml);
        }

        public static explicit operator SmallCompany(XElement xe) { return XTypedServices.ToXTypedElement<SmallCompany>(xe,LinqToXsdTypeManager.Instance as ILinqToXsdTypeManager); }

        public override XTypedElement Clone() {
            return XTypedServices.CloneXTypedElement<SmallCompany>(this);
        }

        /// <summary>
        /// <para>
        /// Regular expression: (CompanyName, Employee+, InfoData+)
        /// </para>
        /// </summary>
        public SmallCompany() {
        }

 //
 // shorten for readibility, full file 780 lines
 ///

Here is the class diagram.

The generated class requires very specific dependencies installed in support.

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 ElemAvalue”. You need to check if element ElemA was present by first checking the flag _ElemAflag; and then if it is true, you go for the value of _ElemAvalue. If you do not check flag _ElemAflag first, and just go for the value of _ElemAvalue 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? ElemAnullableValue”. You need to check if element ElemA was present by first checking the _ElemAnullableValue not being null; and then if it is not, meaning the element was present, you go for the int value of _ElemAnullableValue.

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 ProcessVer3_Process1(
    string? filePath,
    Microsoft.Extensions.Logging.ILogger? logger)
{
    try
    {
        logger?.LogInformation(
            "+++ProcessVer3_Process1-Start++++++++++++++++++");
        logger?.LogInformation("filePath:" + filePath);

        TextReader textReader = File.OpenText(filePath ?? String.Empty);
        //string xmlFile = textReader.ReadToEnd();
        https.markpelf.com.SmallCompany.xsd.SmallCompany xmlObject =
            https.markpelf.com.SmallCompany.xsd.SmallCompany.Load(textReader);

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

            foreach(https.markpelf.com.SmallCompany.xsd.SmallCompany.EmployeeLocalType 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 (https.markpelf.com.SmallCompany.xsd.SmallCompany.InfoDataLocalType item 
                in xmlObject.InfoData)
            {
                logger?.LogInformation("------------");
                logger?.LogInformation("Id_Int_NO:" + item.Id_Int_NO.ToString());

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

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

And here is the log of execution.

 +++ProcessVer3_Process1-Start++++++++++++++++++
 filePath:C:\TmpXSD\XsdExample_Ver3\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_O:123
 ------------
 Id_Int_NO:22
 Quantity_Int_O:null
 +++ProcessVer3_Process1-End++++++++++++++++++

8 Analysis

This tool uses “nullable_type_style” approach/style to mark Optional Xml-Element presence in generated C# code.

  • Quantity_Int_O– is int? type and the meaning is
    a) null - means it was not present
    b) int – present and had value

9 Conclusion

This tool LinqToXsdCore is very interesting and available as freeware. The code C# class generated was very big (it includes some extra methods like Load, Save, Clone), but worked solid in my test. It can be of interest to users who want to use nullable_type_style API, which is generally a more modern approach to handling the Optional Xml-Elements.

Some things make me a bit reserved about using this tool, like why the generated class is so big. Even more than that is the question of those very specific dependencies libraries, are they maintained, and reliable, and will they cause any conflicts with other XML libraries? Answers to these questions would require more studying of this tool before deciding whether to use it in production.

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

[

0
Subscribe to my newsletter

Read articles from Mark Pelf directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mark Pelf
Mark Pelf

A Software Engineer from Belgrade, Serbia.