Today, I was playing with using Saxonica’s .NET API in C# (Microsoft .NET).

The program below was based on the sample here:
c:\Saxonica\Resources\samples\cs\ExamplesHE.cs
You can download the samples from Saxonica on SourceForge in the file that starts with “Resources”.

Sample C# Program

<pre>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

//Do an "Add Reference" to C:\Saxonica\bin\saxon9he-api.dll (where you stored it on your disk)
using Saxon.Api;   //use this, not SaxonAPI - as Saxon.Api contains the "Processor" Class 

namespace SaxonAPI
{
    class Program
    {
        static void Main(string[] args)
        {
            string filename = @"c:\XMLClass\IntroSamples\Flight03.xml";

            // First two examples show how to return a single value, and them multiple values. 
            string xpath1 = "/Reservation/Flight[1]/FlightLeg[1]/FlightNumber/text()";
            string xpath2 = "/Reservation/Flight/FlightLeg/FlightNumber";

            // The third example shows use of XPath 3.0 features, such as the || concatenation operator 
            string xpath3 = "/Reservation/AirlineIATACode || '-' || /Reservation/Flight[1]/FlightLeg[1]/FlightNumber";

            // Call the test methods 
            string resultFlightNumber = DemoXPathSingle(filename, xpath1);
            string resultFlightNumbers = DemoXPathMultiple(filename, xpath2);
            string resultXpath30 = DemoXPathSingle(filename, xpath3);

            Console.WriteLine("result FlightNumber=" + resultFlightNumber);
            Console.WriteLine("result FlightNumbers=" + resultFlightNumbers);
            Console.WriteLine("result XPath 3.0=" + resultXpath30);

            Console.WriteLine("\n\nPress enter to end:");
            Console.ReadLine(); 
        }

        static string  DemoXPathSingle(string parmFilename, string parmXPath)
        {
            
            // Create a Processor instance.
            Processor processor = new Processor();

            // Load the source document
            //XdmNode input = processor.NewDocumentBuilder().Build(new Uri(samplesDir, "data/books.xml"));
            XdmNode input = processor.NewDocumentBuilder().Build(new Uri(parmFilename));

            // Create an XPath compiler
            XPathCompiler xpathCompiler = processor.NewXPathCompiler();

            // Enable caching, so each expression is only compiled once
            xpathCompiler.Caching = true;

            string result = "";

            result = xpathCompiler.EvaluateSingle(parmXPath, input).ToString();
            return result; 

            /*

            // Compile and evaluate some XPath expressions
            foreach (XdmItem item in xpathCompiler.Evaluate(parmXPath, input))
            {

                Console.WriteLine("TITLE: " + xpathCompiler.EvaluateSingle("string(TITLE)", item));
                Console.WriteLine("PRICE: " + xpathCompiler.EvaluateSingle("string(PRICE)", item));
            }

            */

            return result; 

        }

        static string DemoXPathMultiple(string parmFilename, string parmXPath)
        {

            // Create a Processor instance.
            Processor processor = new Processor();

            // Load the source document
            //XdmNode input = processor.NewDocumentBuilder().Build(new Uri(samplesDir, "data/books.xml"));
            XdmNode input = processor.NewDocumentBuilder().Build(new Uri(parmFilename));

            // Create an XPath compiler
            XPathCompiler xpathCompiler = processor.NewXPathCompiler();

            // Enable caching, so each expression is only compiled once
            xpathCompiler.Caching = true;

            // Compile and evaluate some XPath expressions
            /*
            foreach (XdmItem item in xpathCompiler.Evaluate(parmXPath, input))
            {

                Console.WriteLine("TITLE: " + xpathCompiler.EvaluateSingle("string(TITLE)", item));
                Console.WriteLine("PRICE: " + xpathCompiler.EvaluateSingle("string(PRICE)", item));
            }
            */

            // use StringBuild to efficiently build and change a string result in the loop below 
            StringBuilder resultSB = new StringBuilder();
            int loopCounter = 0; 
            foreach (XdmItem item in xpathCompiler.Evaluate(parmXPath, input))
            {
                if (loopCounter++ > 0)
                   {
                    resultSB.Append(", "); 
                   }
                string tempResult = xpathCompiler.EvaluateSingle("string(.)", item).ToString(); 
                resultSB.Append(tempResult);  
            }

            return resultSB.ToString();

}

}  // end class 
}
</pre>

Input XML File

<pre>
<Reservation>
   <ConfirmationCode>QCOCD5</ConfirmationCode>
   <ConfirmationDate>2019-10-16T00:00:00</ConfirmationDate>
   <ExpirationDate>2019-10-10T00:00:00</ExpirationDate>
   <Passenger>Neal Walters</Passenger>
   <RapidRewards>2006721234</RapidRewards>
   <TicketNumber>5261499397436</TicketNumber>
   <PointsEarned>1367</PointsEarned>
   <AirlineIATACode>WN</AirlineIATACode>  
   <AirlineName>Southwest Airlines</AirlineName>
   <BaseFare>227.70</BaseFare>
   <USTransportationTax>17.08</USTransportationTax>
   <US911SecurityFee>11.20</US911SecurityFee>
   <USPassengerFacilityChg>12.30</USPassengerFacilityChg>
   <USFlightSegmentTax>13.50</USFlightSegmentTax>
   <EarlyBird>40.00</EarlyBird>
   <TotalPrice>321.78</TotalPrice>

   <Flight seq="1">
    <FlightLeg seq="1">
       <FlightNumber>1849</FlightNumber>
       <DepartureAirport>MDW</DepartureAirport>
       <ArrivalAirport>STL</ArrivalAirport>
       <DepartureDateTime>2019-11-02T19:20:00</DepartureDateTime>
       <ArrivalDateTime>2019-11-02T20:25:00</ArrivalDateTime>
    </FlightLeg>
    <FlightLeg seq="2">
       <FlightNumber>2105</FlightNumber>
       <DepartureAirport>STL</DepartureAirport>
       <ArrivalAirport>OKC</ArrivalAirport>
       <DepartureDateTime>2019-11-02T21:25:00</DepartureDateTime>
       <ArrivalDateTime>2019-11-02T22:50:00</ArrivalDateTime>
    </FlightLeg>
  </Flight>
   <Flight seq="2">
    <FlightLeg seq="1">
       <FlightNumber>4565</FlightNumber>
       <DepartureAirport>OKC</DepartureAirport>
       <ArrivalAirport>MDW</ArrivalAirport>
       <DepartureDateTime>2019-11-04T11:10:00</DepartureDateTime>
       <ArrivalDateTime>2019-11-04T13:05:00</ArrivalDateTime>
    </FlightLeg>
  </Flight>
</Reservation>
<pre>

Result

<pre>
result FlightNumber=1849
result FlightNumbers=1849, 2105, 4565
result XPath 3.0=WN-1849
</pre>

Frequently in PowerShell, we need to add a directory if it doesn’t already exist.

Simple Code – Check If the Directory Exists First

This assumes the higher level directories exist, and you just want to add the lowest level directory:

<pre>
$targetFilePath = "e:\Dropbox\Audios\Album\Sinatra"
if (!(Test-Path $targetFilePath))
  {
     New-Item -ItemType Directory -Path $targetFilePath | Out-Null 
  }

#
# Now you can write to that new directory, because you know it is there 
#
</pre>

NOTES: If you don’t pipe it to Out-Null, then it will write a message to the console that you may not want.

<pre>
    Directory: e:\Dropbox\Audios\Album\Sinatra

Mode                LastWriteTime         Length Name                                           
----                -------------         ------ ----                                           
da----        5/30/2020  12:55 PM                Sinatra 
</pre>

ShortCut – Use the -Force parameter

This seems like a trick to me, but by specifying the -Force parameter.
The documentation on the -Force parameter says “Forces this cmdlet to create an item that writes over an existing read-only item. Implementation varies from provider to provider.”. The New-Item supports many different providers, one of which is the disk structure.

<pre>
$targetFilePath = "e:\Dropbox\Audios\Album\Sinatra"
New-Item -ItemType Directory -Path $targetFilePath -Force 

#
# Now you can write to that new directory, because you know it is there 
#
</pre>

Create the entire directory, with all parent paths

If you want to create the directory, including the DropBox folder, the Audios folder, and the Album folder, then the following function will do that. Just be careful, because if you misspell one of your higher level directories, it will create them rather than matching to the one you expected to match to.

<pre>
Function GenerateFolder($path) {
    $global:foldPath = $null
    foreach($foldername in $path.split("\")) {
        $global:foldPath += ($foldername+"\")
        if (!(Test-Path $global:foldPath)){
            New-Item -ItemType Directory -Path $global:foldPath
         #or (to avoid the console output)  
         #   New-Item -ItemType Directory -Path $global:foldPath | Out-Null 
            # Write-Host "$global:foldPath Folder Created Successfully"
        }
    }
}

#To Call The Above - note - do not use parentheses for the parameter to PowerShell functions 
GenerateFolder "e:\Dropbox\Audios\Album\Sinatra"
#
# Now you can write to that new directory, because you know it is there 
#

</pre>

GenerateFolder script from: Naredia Reddy on StackOverflow

For example, to put a pound (or hash) sign # in an XML file, you can use either of the following:

ASCII decimal and Hex values in XML

<pre>
 &#35; 
 or
 &#x23; 
</pre>

23 in hex = 2×16+3 = 35 in decimal

So the syntax is the ampersand and the hash tag followed by the number, then followed by a semicolon.
If doing hex, add the letter “x” before the number.

One more example, a Carriage Return/Line Feed:

<pre>
 &#13;&#10; 
 or
 &#x0D;&x0A; 
</pre>

You can find an example ASCII character chart here.

How to encode other characters, such as ampersand, a double or single quote

Other common shortcuts (called entity references) are:

&amp;  ampersand sign 
&lt;  less than sign 
&gt;  greater than sign 
&quot; for a double quote 
&apos; for a single quote (apostrophe) 

CDATA turns off parsing

For long strings where you want to turn off the parser temporarily (in order to allow special characters): '
Other common shortcuts are:

<![CDATA[  anything goes here, such as &, <, > and so on ]]>

Copy and paste from here: 
<![CDATA[  ]]>
and replace the inner brackets with your CDATA text. 

Trademark and Copyright Symbols

Other common symbols are:

Registered Trademark: &#174; and Copyright: &#169;

Query.exe is the implementation of the Saxon XQuery command/engine. It’s a command line program that I’m running from the Windows command prompt.

Issue

<pre>
C:\XMLClass\XQuery>c:\Saxonica\bin\Query.exe -q:Flight3_Xquery.txt -o:flight3_xquery_out.xml
Error on line 2 column 20 of Flight3_Xquery.txt:
  XPDY0002: The context item for axis step root/descendant::FlightLeg is absent
Query failed with dynamic error: The context item for axis step root/descendant::FlightLeg is absent
</pre>

The above is a very confusing error for a simple mistake.
I didn’t pass an input file to the query.exe program with the -s: parameter ((source?), which indicates the xml data file on which to run the query. The -q: parameters (query) specifies the name of the query files itself. And the -o: parameter (output) specifies the output filename for the results of running the XQuery.

Corrected Parameters

<pre>
c:\XMLClass\XQuery>c:\Saxonica\bin\Query.exe -q:Flight3_Xquery.txt -s:c:\XMLClass\IntroSamples\Flight03.xml -o:flight3_xquery_out.xml
</pre>

Sample XQuery

The Flight3.XQuery.txt contains the following sample XQuery:

<pre>
(:  FLWOR = For Let Where Order-by Return   :) 
for $flightLeg in //FlightLeg
where $flightLeg/DepartureAirport = 'OKC' or $flightLeg/ArrivalAirport = 'OKC' 
order by $flightLeg/ArrivalDate[1] descending  
return $flightLeg
</pre>

Second Solution

Instead of passing the input filename on the -s parameter on the command line, the file could be embedded in the XQuery itself with a “doc(filename)” statement, as shown below:

(:  FLWOR = For Let Where Order-by Return   :) 
for $flightLeg in doc("c:\XMLClass\IntroSamples\Flight03.xml")//FlightLeg 
where $flightLeg/DepartureAirport = 'OKC' or $flightLeg/ArrivalAirport = 'OKC' 
order by $flightLeg/ArrivalDate[1] descending  
return $flightLeg

While BizTalk includes an XML to XSD schema utility, we can do the same thing in C# or PowerShell (version 5.1 used in this example, but most of the work is done by .Net classes).

The .NET System.Xml libraries include something interesting called the XmlSchemaInference object.  It can basically look at the XML data file, and reverse engineer a schema from it.  It infers the schema, i.e. it does it’s best to create a schema that can be used to validate the data.

The tricky part is the loop. The sample below shows two ways to handle the loop. Inside the loop, we must pass the XmlWriter variable/object that we created to the Write method.

<pre>
cls
#
# from: http://mylifeismymessage.net/generate-a-schema-from-an-xml-file-in-powershell/
#
$xmlFilename = "c:\XMLClass\IntroSamples\Flight03.xml"
$xsdSchemaFilename = $xmlFilename.Replace(".xml","_dotNet_generated.xsd")

# Remove existing XSD - remove this if you don't want overlay existing file
# or maybe enhance to prompt the user if he wants to overwrite the file. 
if(Test-Path $xsdSchemaFilename)
{
      Remove-Item -Path $xsdSchemaFilename
}

#code converted from C# to PowerShell from here: 
#https://stackoverflow.com/questions/22835730/create-xsd-from-xml-in-code/22836075
#with enhancements and ideas from here: 
#https://learningpcs.blogspot.com/2012/08/powershell-v3-inferring-schema-xsd-from.html

# I left in the C# code so you can see how it gets converted to PowerShell.</pre>
<pre>#XmlReader reader = XmlReader.Create("contosoBooks.xml");
$reader = [System.Xml.XmlReader]::create($xmlFilename)
#XmlSchemaSet schemaSet = new XmlSchemaSet()
#XmlSchemaInference schema = new XmlSchemaInference()
#https://docs.microsoft.com/en-us/dotnet/api/system.xml.schema.xmlschemaset?view=netcore-3.1
$schemaSet = New-Object System.Xml.Schema.XmlSchemaSet
#https://docs.microsoft.com/en-us/dotnet/api/system.xml.schema.xmlschemainference?view=netcore-3.1
$schema = New-Object System.Xml.Schema.XmlSchemaInference 

#schemaSet = schema.InferSchema(reader);
$schemaSet = $schema.InferSchema($reader)

# Create new output file
$file = New-Object System.IO.FileStream($xsdSchemaFilename, [System.IO.FileMode]::CreateNew)
$file.Close() 

$xmlWriter = New-Object System.Xml.XmlTextWriter ($xsdSchemaFilename, [System.Text.Encoding]::UTF8)
$xmlWriter.Formatting = [System.Xml.Formatting]::Indented

$loopCounter = 0 
foreach ($s in $schemaSet.Schemas())
{
   $loopCounter++
   Write-Host "LoopCounter: $loopCounter"
   $s.Write($xmlWriter) 
   
}

$xmlWriter.Close()
Write-Host "See file: $xsdSchemaFilename"

</pre>

>