From 15370b3f5399b0ba708e3a8394bf61d58a80ba41 Mon Sep 17 00:00:00 2001 From: Karl Kauc Date: Sun, 30 Aug 2026 15:48:15 +0200 Subject: [PATCH] fix(schematron): .NET variant drives the SchXslt CLI jar - builds, runs, in CI The .NET Schematron example referenced SaxonHE12NetXslt, which NuGet publishes as a dotnet *tool*, so the project never restored (NU1212). There is no Saxon-HE library package for .NET 8 at all: Saxonica ships its .NET builds as tools only, SaxonCS is not on NuGet, and the third-party IKVM cross-compiles (SaxonHE10Net31Api) fail at runtime with a missing Saxon.HE assembly. Replace the in-process Saxon pipeline with what a .NET service in a mixed shop actually does: run the self-contained SchXslt CLI jar (bundles its own Saxon) as a child process (java from $JAVA_HOME or PATH; jar from $FUNDSXML_SCHXSLT_JAR or the Maven local repo, as before) and own the result - SVRL parsing, error/warning classification by @role (failed-assert and successful-report alike) and the 0/1/2 exit-code contract shared with the Java example and svrl-summary.py. No NuGet dependencies remain. Verified (.NET SDK 8, JDK 26): canonical sample 0 errors / 12 warnings exit 0; negative fixture 1 error exit 1; --fail-on any exit 1; missing jar and missing document exit 2 with a one-line message. CI runs the sample and the fixture through it after the Java module has populated the Maven repo. invocation/README.md row marked verified. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01YXZWhwE5EbHSybt55K6oUZ --- .github/workflows/ci.yml | 11 +++ .../Basic_Checks/invocation/README.md | 2 +- .../invocation/SchematronValidate.cs | 97 +++++++++++-------- .../invocation/SchematronValidate.csproj | 27 ++---- 4 files changed, 75 insertions(+), 62 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94f0b14..ecf8207 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -173,6 +173,17 @@ jobs: fi echo "schematron-invalid correctly failed" + - name: Schematron - .NET variant (drives the SchXslt CLI jar from the Maven local repo) + run: | + set -e + SCH=Schematron_DataQuality_Checks/Basic_Checks/basic_checks.sch + D="dotnet run --project Schematron_DataQuality_Checks/Basic_Checks/invocation --" + $D $SCH FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml + if $D $SCH tests/fixtures/invalid/schematron-invalid_Positions.xml; then + echo "::error::schematron-invalid fixture unexpectedly passed (.NET)"; exit 1 + fi + echo ".NET SchematronValidate: sample ok, fixture correctly failed" + - name: Smoke - XSLT transforms produce non-empty output run: | set -e diff --git a/Schematron_DataQuality_Checks/Basic_Checks/invocation/README.md b/Schematron_DataQuality_Checks/Basic_Checks/invocation/README.md index a409a64..779d8c6 100644 --- a/Schematron_DataQuality_Checks/Basic_Checks/invocation/README.md +++ b/Schematron_DataQuality_Checks/Basic_Checks/invocation/README.md @@ -39,7 +39,7 @@ exits 1 on any failed-assert (including warnings). |-------|------|----------------------| | Java (native) | [`SchematronValidate.java`](SchematronValidate.java) | ✅ verified (SchXslt Java API, via Maven Wrapper) | | Python | [`validate_schematron.py`](validate_schematron.py) | saxonche via repo venv (`pip install -e .`); SchXslt jar via `$FUNDSXML_SCHXSLT_JAR` or Maven local repo — reference variant | -| .NET/C# | [`SchematronValidate.cs`](SchematronValidate.cs) | reference variant, **does not restore as-is**: Saxonica publishes its .NET packages (`SaxonHE12Net*`) as dotnet *tools*, not libraries (`NU1212`); pair the code with a Saxon .NET library build of your own. SchXslt jar via `$FUNDSXML_SCHXSLT_JAR` or Maven local repo | +| .NET/C# | [`SchematronValidate.cs`](SchematronValidate.cs) | ✅ verified (.NET SDK 8, in CI): no NuGet deps — runs the self-contained SchXslt CLI jar (`$FUNDSXML_SCHXSLT_JAR` or Maven local repo) as a child process and classifies the SVRL itself; needs a JDK on PATH/`$JAVA_HOME` | | shared | [`svrl-summary.py`](svrl-summary.py) | ✅ classifier used by all + CI | The Java example runs standalone and cross-platform via the committed Maven diff --git a/Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.cs b/Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.cs index 259dd26..46309cb 100644 --- a/Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.cs +++ b/Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.cs @@ -1,27 +1,32 @@ -// Schematron validation in .NET / C# via Saxon for .NET (Saxonica SaxonHE). +// Schematron validation in .NET / C# — drives the SchXslt CLI, classifies SVRL. // -// # add the Saxon-HE-for-.NET package matching your TFM (see .csproj note) // dotnet run --project Schematron_DataQuality_Checks/Basic_Checks/invocation \ -// -- Schematron_DataQuality_Checks/Basic_Checks/basic_checks.sch document.xml +// -- Schematron_DataQuality_Checks/Basic_Checks/basic_checks.sch document.xml [--fail-on error|any] // Exit: 0 = no error-role failed-assert, 1 = at least one, 2 = setup error. // -// basic_checks.sch uses queryBinding="xslt2"; the SaxonHE NuGet package -// supplies the XSLT 3.0 engine (resolved by `dotnet build`, standalone). The -// SchXslt pipeline stylesheets have no NuGet/.NET distribution, so they are -// reused from the SchXslt CLI jar, located via $FUNDSXML_SCHXSLT_JAR or the -// Maven local repo (see below); the whole xslt/ tree is extracted so the -// pipeline's relative imports resolve, then compile .sch -> SVRL stylesheet -// -> apply to instance -> SVRL, then classify (same logic as svrl-summary.py). +// WHY NOT A .NET XSLT ENGINE +// basic_checks.sch uses queryBinding="xslt2". The .NET BCL only has XSLT 1.0 +// (System.Xml.Xsl), and there is no Saxon-HE library on NuGet for .NET 8: +// Saxonica publishes its .NET packages (SaxonHE12Net*) as dotnet *tools*, +// SaxonCS is not on NuGet, and the third-party IKVM cross-compiles are +// experimental. So this example does what a .NET service in a mixed shop +// typically does: it runs the SchXslt CLI (a self-contained jar that bundles +// its own Saxon) as a child process and owns the result — the SVRL parsing, +// the error/warning classification and the exit-code contract are the same +// as in svrl-summary.py and the Java example. Prerequisite: a JDK on PATH +// (or $JAVA_HOME) — the same one the Maven Wrapper uses. // -// NOTE: reference variant. The Java Schematron example is the verified, fully -// standalone path (Maven Wrapper). The flow here mirrors it exactly. +// The SchXslt CLI jar has no NuGet distribution; it is located standalone via +// $FUNDSXML_SCHXSLT_JAR or the Maven local repo (populated by the Java module). +// +// Verified with .NET SDK 8 + JDK 21/26 (also in CI): canonical sample -> 0 +// errors / 12 warnings, negative fixture -> 1 error / exit 1. using System; +using System.Diagnostics; using System.IO; -using System.IO.Compression; using System.Linq; using System.Xml; -using Saxon.Api; internal static class SchematronValidate { @@ -66,34 +71,50 @@ private static int Main(string[] args) } string tmp = Directory.CreateTempSubdirectory().FullName; - using (var zip = ZipFile.OpenRead(cliJar)) - foreach (var e in zip.Entries.Where(e => e.FullName.StartsWith("xslt/") - && !e.FullName.EndsWith("/"))) - { - string dest = Path.Combine(tmp, e.FullName); - Directory.CreateDirectory(Path.GetDirectoryName(dest)!); - e.ExtractToFile(dest, true); - } - - var processor = new Processor(false); - var comp = processor.NewXsltCompiler(); - - string compiled = Path.Combine(tmp, "compiled.xsl"); string svrl = Path.Combine(tmp, "report.svrl"); - // 1) Schematron -> SVRL stylesheet - Transform(comp, Path.Combine(tmp, "xslt/2.0/pipeline-for-svrl.xsl"), - sch, compiled, processor); - // 2) instance -> SVRL - Transform(comp, compiled, xml, svrl, processor); + // java from $JAVA_HOME if set (what the Maven Wrapper honours), else PATH. + string? javaHome = Environment.GetEnvironmentVariable("JAVA_HOME"); + string java = string.IsNullOrEmpty(javaHome) ? "java" + : Path.Combine(javaHome, "bin", "java"); + + // SchXslt CLI: -s schema, -d document, -o SVRL output. Its own exit code + // is left at the default (0) on purpose — the classification below + // decides, exactly like the Java example and svrl-summary.py. + var psi = new ProcessStartInfo(java) + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }; + foreach (var a in new[] { "-jar", cliJar, "-s", sch, "-d", xml, "-o", svrl }) + psi.ArgumentList.Add(a); + using (var proc = Process.Start(psi)!) + { + // Drain both pipes concurrently so a chatty child cannot block. + var stdoutTask = proc.StandardOutput.ReadToEndAsync(); + string stderr = proc.StandardError.ReadToEnd(); + stdoutTask.Wait(); + proc.WaitForExit(); + if (proc.ExitCode != 0 || !File.Exists(svrl)) + { + Console.Error.WriteLine("SchXslt CLI failed (exit " + + proc.ExitCode + "):\n" + stderr.Trim()); + return 2; + } + } var doc = new XmlDocument(); doc.Load(svrl); var ns = new XmlNamespaceManager(doc.NameTable); ns.AddNamespace("svrl", Svrl); + // Severity is the @role attribute, not the element name: this ruleset + // raises warnings as (=> failed-assert) and the + // rounding checks as (=> successful-report). int errors = 0, warnings = 0; - foreach (XmlElement fa in doc.SelectNodes("//svrl:failed-assert", ns)!) + foreach (XmlElement fa in doc.SelectNodes( + "//svrl:failed-assert | //svrl:successful-report", ns)!) { string role = fa.GetAttribute("role").ToLowerInvariant(); string text = (fa.SelectSingleNode("svrl:text", ns)?.InnerText ?? "") @@ -106,14 +127,4 @@ private static int Main(string[] args) if (failOn == "any" && (errors > 0 || warnings > 0)) return 1; return errors > 0 ? 1 : 0; } - - private static void Transform(XsltCompiler comp, string xsl, string src, - string outFile, Processor p) - { - var exe = comp.Compile(new Uri(Path.GetFullPath(xsl))); - var t = exe.Load30(); - using var os = File.Create(outFile); - t.Transform(new Uri(Path.GetFullPath(src)), - p.NewSerializer(os)); - } } diff --git a/Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.csproj b/Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.csproj index 3c85a54..9177555 100644 --- a/Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.csproj +++ b/Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.csproj @@ -1,11 +1,13 @@ - + Exe net8.0 @@ -13,15 +15,4 @@ SchematronValidate FundsXml.Schematron - - - -