Skip to content

Commit 723c982

Browse files
committed
Add nullable types and modern C# conventions; refactor code and improve error handling
- Introduced `XPathHelpers` for safer navigation and XML handling. - Updated `Validator`, `SchemaLoader`, and other classes to use new helper methods. - Refactored exception handling to remove unnecessary constructors. - Enhanced `Config` and `PhaseCollection` for better type safety. - Updated project metadata and README for clarity and accuracy. - Added new icon and markdown documentation.
1 parent ffa3b0c commit 723c982

19 files changed

Lines changed: 360 additions & 169 deletions

assets/img/icon.png

2.07 MB
Loading

readme.md

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,160 @@
11
# Schematron.NET
22

3-
This is a [Schematron ISO/IEC 2025](http://www.schematron.com/) standard processor for .NET, written in C#.
3+
[![Version](https://img.shields.io/nuget/vpre/Schematron.svg?color=royalblue)](https://www.nuget.org/packages/Schematron)
4+
[![Downloads](https://img.shields.io/nuget/dt/Schematron.svg?color=darkmagenta)](https://www.nuget.org/packages/Schematron)
5+
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/devlooped/Schematron/blob/main/license.txt)
6+
[![GitHub](https://img.shields.io/badge/-source-181717.svg?logo=GitHub)](https://github.com/devlooped/Schematron)
7+
8+
<!-- #description -->
9+
A .NET implementation of Schematron for validating XML with standalone `.sch` schemas or
10+
Schematron rules embedded in W3C XML Schema. The library supports both the current ISO
11+
Schematron namespace and the legacy ASCC namespace, with a compact public API centered on
12+
`Validator` and `Schema`.
13+
<!-- #description -->
14+
15+
<!-- include https://github.com/devlooped/.github/raw/main/osmf.md -->
16+
17+
<!-- #content -->
18+
## Installation
19+
20+
```shell
21+
dotnet add package Schematron
22+
```
23+
24+
The package targets `netstandard2.0` and `net8.0`.
25+
26+
## Usage
27+
28+
The package exposes two primary entry points:
29+
30+
- `Validator` loads Schematron and XML Schema definitions and validates XML documents.
31+
- `Schema` loads and inspects Schematron documents programmatically.
32+
33+
### One-shot validation with a standalone Schematron schema
34+
35+
```csharp
36+
using Schematron;
37+
38+
var validator = new Validator();
39+
validator.AddSchema("order.sch");
40+
41+
try
42+
{
43+
validator.Validate("order.xml");
44+
Console.WriteLine("Document is valid.");
45+
}
46+
catch (ValidationException ex)
47+
{
48+
Console.WriteLine(ex.Message);
49+
}
50+
```
51+
52+
`Validate` returns the loaded `IXPathNavigable` document on success and throws
53+
`ValidationException` when XML Schema or Schematron validation fails.
54+
55+
### Validating XSD plus embedded Schematron
56+
57+
When the schema is a W3C XML Schema document with embedded Schematron, `Validator` runs
58+
both validations in one pass:
59+
60+
```csharp
61+
using Schematron;
62+
63+
var validator = new Validator(OutputFormatting.XML);
64+
65+
// Use the overload with the target namespace when the XSD imports or includes other schemas.
66+
validator.AddSchema("http://example.com/po-schematron", "po-schema.xsd");
67+
68+
try
69+
{
70+
validator.Validate("purchase-order.xml");
71+
}
72+
catch (ValidationException ex)
73+
{
74+
Console.WriteLine(ex.Message); // XML formatted output
75+
}
76+
```
77+
78+
### Schematron-only validation for in-memory XML
79+
80+
If you already have an `IXPathNavigable`, use `ValidateSchematron` to skip XML Schema
81+
validation and evaluate only the loaded Schematron rules:
82+
83+
```csharp
84+
using System.Xml.XPath;
85+
using Schematron;
86+
87+
var validator = new Validator();
88+
validator.AddSchema("rules.sch");
89+
90+
var document = new XPathDocument("order.xml");
91+
validator.ValidateSchematron(document);
92+
```
93+
94+
### Selecting the phase, formatter, and return type
95+
96+
`Validator` lets you control the active phase, output format, and result type:
97+
98+
```csharp
99+
using Schematron;
100+
using Schematron.Formatters;
101+
102+
var validator = new Validator(OutputFormatting.XML, NavigableType.XmlDocument)
103+
{
104+
Phase = "paymentInfo",
105+
Formatter = new XmlFormatter(),
106+
};
107+
108+
validator.AddSchema("purchase-order.sch");
109+
var result = validator.Validate("purchase-order.xml");
110+
```
111+
112+
Use `Phase.All` (`#ALL`) to evaluate every pattern regardless of phase activation.
113+
114+
### Loading and inspecting a schema
115+
116+
Use `Schema` directly when you want to inspect a Schematron document without validating
117+
an instance document yet:
118+
119+
```csharp
120+
using Schematron;
121+
122+
var schema = new Schema();
123+
schema.Load("rules.sch");
124+
125+
Console.WriteLine(schema.Title);
126+
Console.WriteLine(schema.SchematronEdition);
127+
Console.WriteLine(schema.DefaultPhase);
128+
Console.WriteLine(schema.IsLibrary);
129+
Console.WriteLine(schema.Patterns.Count);
130+
Console.WriteLine(schema.Lets.Contains("maxAge"));
131+
```
132+
133+
This is useful for tooling, analyzers, test helpers, or apps that need to inspect declared
134+
phases, patterns, diagnostics, parameters, and `let` bindings.
135+
136+
## Core API
137+
138+
Schematron keeps the public surface intentionally small:
139+
140+
- `Validator` is the main entry point for loading schemas and validating XML.
141+
- `Schema` loads and inspects Schematron documents programmatically.
142+
- `OutputFormatting` selects the built-in output style: `Default`/`Log`, `Simple`,
143+
`Boolean`, or `XML`.
144+
- `Validator.Phase`, `Validator.Formatter`, and `Validator.ReturnType` let you choose
145+
the active phase, custom output formatter, and returned document type.
146+
- `BadSchemaException` signals invalid schema input, while `ValidationException`
147+
signals XML or Schematron validation failures.
148+
149+
## Supported features
150+
151+
The package currently supports the main scenarios expected for a v1 release, including:
152+
153+
- Standalone `.sch` schemas and Schematron embedded in W3C XML Schema
154+
- ISO Schematron namespace (`http://purl.oclc.org/dsdl/schematron`) and legacy ASCC namespace compatibility
155+
- ISO Schematron 2025 features such as `<library>`, phase `@when`, rule `@visit-each`, rule flags, and schema parameters
156+
- Schema-, pattern-, and rule-level `let` bindings
157+
- Diagnostics, severity metadata, abstract patterns/rules, and groups
158+
<!-- #content -->
159+
160+
<!-- include https://github.com/devlooped/sponsors/raw/main/footer.md -->

src/Directory.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<Product>Schematron.NET</Product>
55
<Description>A C# high-performance implementation of Schematron ISO/IEC standard</Description>
6-
<PackageProjectUrl>https://github.com/kzu/Schematron</PackageProjectUrl>
6+
<PackageProjectUrl>https://github.com/devlooped/Schematron</PackageProjectUrl>
77
<ImplicitUsings>true</ImplicitUsings>
88
</PropertyGroup>
99

src/Schematron/BadSchemaException.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,6 @@ public BadSchemaException() { }
2121
/// <param name="message">The error message that explains the reason for the exception.</param>
2222
public BadSchemaException(string message) : base(message) { }
2323

24-
/// <summary>
25-
/// Initializes a new instance of the <see cref="BadSchemaException"/> class.
26-
/// </summary>
27-
/// <param name="info">Info</param>
28-
/// <param name="context">Context</param>
29-
protected BadSchemaException(SerializationInfo info, StreamingContext context) : base(info, context) { }
30-
3124
/// <summary>
3225
/// Initializes a new instance of the <see cref="BadSchemaException"/> class.
3326
/// </summary>

src/Schematron/Config.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ static Config()
3535
full.Phases.Add(full.CreatePhase(Phase.All));
3636

3737
//TODO: should we move all the schema language elements to a resource file?
38-
navigator = new XmlDocument().CreateNavigator();
38+
navigator = new XmlDocument().CreateRequiredNavigator();
3939
navigator.NameTable.Add("active");
4040
navigator.NameTable.Add("pattern");
4141
navigator.NameTable.Add("assert");
@@ -146,4 +146,4 @@ public static void Setup()
146146
System.Diagnostics.Trace.Write(TagExpressions.Dir().RightToLeft);
147147
System.Diagnostics.Trace.WriteLine(FormattingUtils.XmlErrorPosition.RightToLeft);
148148
}
149-
}
149+
}

src/Schematron/Formatters/FormatterBase.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Xml;
33
using System.Xml.Schema;
44
using System.Xml.XPath;
5+
using Schematron;
56

67
namespace Schematron.Formatters;
78
/// <summary>
@@ -115,7 +116,7 @@ protected static StringBuilder FormatMessage(Test source, XPathNavigator context
115116
// name of the first node, which is compatible with XSLT implementation.
116117
var nodes = (XPathNodeIterator)context.Evaluate(nameExpr);
117118
if (nodes.MoveNext())
118-
result = nodes.Current.Name;
119+
result = nodes.CurrentOrThrow().Name;
119120
}
120121
else
121122
{
@@ -136,7 +137,7 @@ protected static StringBuilder FormatMessage(Test source, XPathNavigator context
136137
var nodes = (XPathNodeIterator)context.Evaluate(selectExpr);
137138
result = string.Empty;
138139
while (nodes.MoveNext())
139-
result += nodes.Current.Value;
140+
result += nodes.CurrentOrThrow().Value;
140141
}
141142
else
142143
{

src/Schematron/Formatters/FormattingUtils.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,12 @@ public static string GetFullNodePosition(XPathNavigator context, string previous
5353
{
5454
if (context.Prefix == string.Empty)
5555
{
56-
pref = source.GetContext()!.LookupPrefix(source.GetContext()!.NameTable.Get(context.NamespaceURI));
56+
var sourceContext = source.GetContext();
57+
var nameTable = sourceContext?.NameTable;
58+
var namespaceName = nameTable?.Get(context.NamespaceURI);
59+
pref = sourceContext is null || namespaceName is null
60+
? string.Empty
61+
: sourceContext.LookupPrefix(namespaceName) ?? string.Empty;
5762
}
5863
else
5964
{
@@ -62,9 +67,9 @@ public static string GetFullNodePosition(XPathNavigator context, string previous
6267

6368
if (!namespaces.ContainsKey(context.NamespaceURI))
6469
{
65-
namespaces.Add(context.NamespaceURI, pref ?? "");
70+
namespaces.Add(context.NamespaceURI, pref);
6671
}
67-
else if (((string)namespaces[context.NamespaceURI]) != pref &&
72+
else if ((namespaces[context.NamespaceURI] as string ?? string.Empty) != pref &&
6873
!namespaces.ContainsKey(context.NamespaceURI + ":" + pref))
6974
{
7075
namespaces.Add(context.NamespaceURI + " " + pref, pref);
@@ -132,7 +137,7 @@ public static string GetNodeSummary(XPathNavigator context, Hashtable namespaces
132137
// Get the element name
133138
XmlQualifiedName name;
134139
if (ctx.NamespaceURI != string.Empty)
135-
name = new XmlQualifiedName(ctx.LocalName, namespaces[ctx.NamespaceURI].ToString());
140+
name = new XmlQualifiedName(ctx.LocalName, namespaces[ctx.NamespaceURI] as string ?? string.Empty);
136141
else
137142
name = new XmlQualifiedName(ctx.LocalName);
138143

@@ -165,15 +170,15 @@ public static string GetNamespaceSummary(XPathNavigator context, Hashtable names
165170
foreach (var key in keys)
166171
{
167172
sb.Append(spacing).Append("xmlns");
168-
pref = namespaces[key].ToString();
173+
pref = namespaces[key] as string ?? string.Empty;
169174

170175
if (pref != string.Empty)
171176
sb.Append(":").Append(namespaces[key]);
172177

173178
sb.Append("=\"");
174179

175180
if (pref != string.Empty)
176-
sb.Append(removeprefix.Replace(key.ToString(), string.Empty));
181+
sb.Append(removeprefix.Replace(key?.ToString() ?? string.Empty, string.Empty));
177182
else
178183
sb.Append(key);
179184

src/Schematron/Formatters/XmlFormatter.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Xml;
44
using System.Xml.Schema;
55
using System.Xml.XPath;
6+
using Schematron;
67

78
namespace Schematron.Formatters;
89

@@ -118,8 +119,14 @@ public override void Format(Schema source, XPathNavigator context, StringBuilder
118119
foreach (string prefix in source.NsManager)
119120
{
120121
if (!prefix.StartsWith("xml"))
121-
writer.WriteAttributeString("xmlns", prefix, null,
122-
source.NsManager.LookupNamespace(source.NsManager.NameTable.Get(prefix)));
122+
{
123+
var nameTable = source.NsManager.NameTable
124+
?? throw new InvalidOperationException("The namespace manager does not have an associated name table.");
125+
var namespacePrefix = nameTable.Get(prefix) ?? prefix;
126+
var namespaceUri = source.NsManager.LookupNamespace(namespacePrefix);
127+
if (namespaceUri is not null)
128+
writer.WriteAttributeString("xmlns", prefix, null, namespaceUri);
129+
}
123130
}
124131

125132
if (source.Title != string.Empty) writer.WriteAttributeString("title", source.Title);

src/Schematron/InvalidExpressionException.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,6 @@ public InvalidExpressionException() : base() { }
1818
/// <param name="message">The error message that explains the reason for the exception.</param>
1919
public InvalidExpressionException(string message) : base(message) { }
2020

21-
/// <summary>
22-
/// For serialization purposes.
23-
/// </summary>
24-
/// <param name="info">Info</param>
25-
/// <param name="context">Context</param>
26-
protected InvalidExpressionException(SerializationInfo info, StreamingContext context) : base(info, context) { }
27-
2821
/// <summary>
2922
/// Initializes an instance of the class with a specified error message
3023
/// and a reference to the inner exception that is the cause for this exception.

src/Schematron/PhaseCollection.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections;
2+
using System.Collections.Generic;
23

34
namespace Schematron;
45

@@ -14,7 +15,8 @@ public PhaseCollection()
1415
/// <summary>Required indexer.</summary>
1516
public Phase this[string key]
1617
{
17-
get { return (Phase)Dictionary[key]; }
18+
get => Dictionary[key] as Phase
19+
?? throw new KeyNotFoundException($"The phase '{key}' was not found.");
1820
set { Dictionary[key] = value; }
1921
}
2022

0 commit comments

Comments
 (0)