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>
<?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 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
============================
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;
}
}
}
}
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:
- 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).
- 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);
}
}
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++++++++++++++++++
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