惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
Cyberwarzone
Cyberwarzone
The GitHub Blog
The GitHub Blog
云风的 BLOG
云风的 BLOG
P
Proofpoint News Feed
小众软件
小众软件
Recent Announcements
Recent Announcements
博客园 - 三生石上(FineUI控件)
Security Archives - TechRepublic
Security Archives - TechRepublic
W
WeLiveSecurity
Cloudbric
Cloudbric
博客园 - 司徒正美
美团技术团队
N
News and Events Feed by Topic
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
PCI Perspectives
PCI Perspectives
宝玉的分享
宝玉的分享
H
Help Net Security
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Google DeepMind News
Google DeepMind News
Help Net Security
Help Net Security
Last Week in AI
Last Week in AI
S
Schneier on Security
N
News | PayPal Newsroom
B
Blog RSS Feed
L
LINUX DO - 最新话题
T
Troy Hunt's Blog
S
Secure Thoughts
雷峰网
雷峰网
aimingoo的专栏
aimingoo的专栏
L
Lohrmann on Cybersecurity
G
Google Developers Blog
Microsoft Azure Blog
Microsoft Azure Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
T
Tenable Blog
S
Securelist
L
LangChain Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
I
InfoQ
H
Heimdal Security Blog
Cisco Talos Blog
Cisco Talos Blog
F
Full Disclosure
Y
Y Combinator Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
K
Kaspersky official blog
T
Tailwind CSS Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
阮一峰的网络日志
阮一峰的网络日志
C
Cisco Blogs

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Why validating Peppol UBL e-invoices in .NET is harder than it looks
Stefan Meier · 2026-04-27 · via DEV Community

Why validating Peppol UBL e-invoices in .NET is harder than it looks

When I started building InvoiceXML (a REST API for Peppol and EN 16931 e-invoice compliance), one of the first technical walls I hit was EN 16931 Schematron validation in .NET. I assumed it would be straightforward. It was not.

This post is about that journey: what Peppol UBL validation actually requires under the hood, why .NET makes it particularly awkward, what the community workarounds look like, and why I eventually decided none of them were good enough for a production compliance service.

If you are building e-invoicing into a .NET application, whether for Belgium's January 2026 B2B mandate, the broader Peppol network, ZUGFeRD/Factur-X for Germany and France, or XRechnung for German public sector, this problem applies to all of them equally.


What Peppol UBL validation actually involves

Most developers, when they first encounter Peppol e-invoicing, assume "validate the invoice" means "check the XML schema." Run the UBL XSD, confirm the document is well-formed, move on.

That is step one. The real validation is step two: Schematron.

EN 16931 defines 200+ business rules that go beyond what XSD can express. Rules like:

  • BR-CO-14: Invoice total VAT amount must equal the sum of all VAT category tax amounts
  • BR-61: When payment means code is 30 or 58, a payment account identifier (IBAN) must be present
  • BR-AE-05: When VAT category is reverse charge, the VAT rate must be zero
  • BR-01: The invoice must declare a specification identifier

Then on top of EN 16931, Peppol BIS Billing 3.0 adds its own overlay rules:

  • PEPPOL-EN16931-R004: Invoice type code must be 380 for a standard invoice
  • PEPPOL-EN16931-R010: Buyer endpoint identifier must use a valid Peppol scheme
  • Additional rules around party identifiers, payment references, and document structure

These rules check semantic correctness across the entire document - arithmetic consistency, cross-field relationships, conditional requirements. XSD cannot express any of this. You need Schematron.

Schematron is an ISO standard for rule-based XML validation. The EN 16931 and Peppol BIS Schematron artefacts are XSLT files, specifically XSLT 2.0, that you run against the invoice XML to produce an SVRL (Schematron Validation Report Language) output listing every rule violation.

Here is the problem: XSLT 2.0 has no official native .NET implementation.

The same problem applies to every EN 16931-based format. ZUGFeRD and Factur-X use CII XML inside a PDF/A-3b container, same Schematron requirement. XRechnung adds its own CIUS-REC-DE national extension rules on top. Peppol BIS updates its Schematron quarterly. Every format, same underlying wall.


The .NET XSLT 2.0 problem

.NET has built-in XSLT support via System.Xml.Xsl.XslCompiledTransform. It supports XSLT 1.0. The EN 16931 Schematron artefacts require XSLT 2.0. XslCompiledTransform will not run them, it throws immediately on XSLT 2.0 constructs.

In Java, the standard solution is Saxon-HE, a mature, well-supported XSLT 2.0/3.0 processor from Saxonica. It works, it is what every Java-based e-invoicing tool uses, and it has official support.

For .NET there is no official Saxon port. What exists is a community effort: IKVM, a tool that cross-compiles Java bytecode to .NET assemblies. Someone took Saxon-HE, ran it through IKVM, and published the result as a NuGet package. This is the SaxonHE12s9apiExtensions package you will find if you search for "Saxon .NET Core."

Let me show you what actually using this looks like in practice.


The IKVM approach: what it looks like in code

// NuGet packages required:
// SaxonHE12s9apiExtensions
// IKVM.Runtime
// IKVM.Runtime.win-x64 (+ linux-x64, osx-x64, osx-arm64 for cross-platform)

using net.sf.saxon.s9api;

var processor = new Processor(false);
var compiler = processor.newXsltCompiler();

// Load the Peppol BIS Schematron XSLT (embedded as resource)
using var schematronStream = Assembly
    .GetExecutingAssembly()
    .GetManifestResourceStream("MyApp.Resources.peppolbis-en16931-ubl.xslt");

var executable = compiler.compile(
    new javax.xml.transform.stream.StreamSource(
        new java.io.InputStream.Wrapper(schematronStream)
    )
);

var transformer = executable.load30();

var invoiceSource = new javax.xml.transform.stream.StreamSource(
    new java.io.StringReader(invoiceXml)
);

var destination = new net.sf.saxon.s9api.XdmDestination();
transformer.applyTemplates(invoiceSource, destination);

// Now parse the SVRL output XML to find violations
var svrl = destination.getXdmNode().toString();
// ... parse SVRL XML
// ... extract failed-assert elements
// ... read @location, @test, and text() from each
// ... filter fatal errors from warnings
// ... map technical rule codes to human-readable messages
// ... figure out which line item the violation refers to

Enter fullscreen mode Exit fullscreen mode

If you got to this point and it compiled and ran, congratulations. Before you got here you also needed to:

  • Identify the correct combination of IKVM NuGet packages (there are several, they do not all work together on every .NET version)
  • Handle platform-specific native libraries, IKVM ships separate packages per runtime identifier and you need all of them for cross-platform deployment
  • Embed the correct Schematron XSLT files as EmbeddedResource in your project
  • Build the SVRL parsing layer entirely yourself, the IKVM/Saxon output is raw XML that you then need to traverse to extract rule violations, their locations, and their messages
  • Deal with Java type bridging throughout your C# codebase: javax.xml.transform.stream.StreamSource, java.io.InputStream.Wrapper, net.sf.saxon.s9api, Java namespaces sitting in the middle of your C# application

That last point is worth pausing on. When you see javax.xml.transform in a C# file, something has gone architecturally sideways. It works (right now, with this combination of package versions, on this runtime). But it is not a foundation.


Why this is not something you want in production

Here is the specific failure surface you are accepting:

A four-way version compatibility matrix. Your validation depends on Saxon-HE (Saxonica's release schedule), IKVM (community project, no commercial support, no SLA), the NuGet package publisher maintaining the cross-compilation, and your .NET runtime version. All four need to be compatible simultaneously. When they fall out of sync, and they do, your validation stops working. You find out when a customer reports an unhandled exception.

Quarterly Schematron updates. OpenPeppol releases new Peppol BIS artefacts roughly every quarter. Each release requires you to download the new XSLT files from the OpenPeppol GitHub repository, embed them in your application, regression-test them against your invoice samples (because occasionally a rule's semantics change, not just its wording), and deploy. Miss a release and your validation accepts invoices that the Peppol network rejects. Your customer's invoice gets bounced and they call your support line.

ZUGFeRD, Factur-X, and XRechnung add their own update cycles. FeRD releases new ZUGFeRD/Factur-X versions roughly annually. KoSIT releases new XRechnung CIUS versions annually. Each has its own Schematron artefacts you need to track and update independently.

The SVRL parsing layer is entirely your problem. IKVM/Saxon gives you raw SVRL output — an XML document listing violations with XPath locations and technical rule assertions. Turning that into structured error objects with rule codes, severity levels, human-readable messages, and line item references is a non-trivial parsing and mapping exercise that you write, own, and maintain.

Java bleeds into your codebase permanently. Every developer who joins your team has to understand why there are Java package names in the C# source. Every code review touching the validation layer requires context that is not obvious from reading .NET documentation. It is a maintenance cost that compounds quietly over time.


What we built instead

When building InvoiceXML we looked at this honestly and decided that running cross-compiled Java inside a .NET compliance service, where the output determines whether legally significant financial documents are accepted or rejected, was not an acceptable foundation. We built our own validation infrastructure properly.

The result is a REST endpoint that accepts a UBL invoice, ZUGFeRD or Factur-X PDF, XRechnung XML, or standalone CII document and returns structured validation results in JSON. For UBL it runs the EN 16931 Schematron, detects the CustomizationID profile automatically, and applies the correct CIUS overlay (Peppol BIS 3.0, NLCIUS, EHF, PINT, or XRechnung) in a single call.

From a .NET application, the integration is clean:

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", apiKey);

using var form = new MultipartFormDataContent();
form.Add(
    new StreamContent(File.OpenRead("invoice-peppol.xml")),
    "file",
    "invoice-peppol.xml"
);

var response = await client.PostAsync(
    "https://api.invoicexml.com/v1/validate/ubl",
    form
);

var result = await response.Content
    .ReadFromJsonAsync<ValidationResult>();

if (!result.Valid)
{
    foreach (var error in result.Errors.Friendly)
    {
        Console.WriteLine($"[{error.Rule}] [{error.Layer}] {error.Message}");
        // e.g. [BR-CO-14] [en16931] Invoice total VAT does not match...
        // e.g. [PEPPOL-EN16931-R004] [peppol-bis] Invoice type code must be 380...
    }
}

Enter fullscreen mode Exit fullscreen mode

The response:

{
  "valid": false,
  "detail": "Validation failed with 2 error(s)",
  "data": {
    "detectedProfile": "Peppol BIS Billing 3.0",
    "documentType": "Invoice",
    "conformanceLevel": "EN16931",
    "customizationId": "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0"
  },
  "errors": {
    "xml": [
      "[BR-CO-14] Invoice total VAT amount must equal the sum of VAT category tax amounts.",
      "[PEPPOL-EN16931-R004] Invoice type code should be 380 for a standard invoice."
    ],
    "friendly": [
      {
        "rule": "BR-CO-14",
        "layer": "en16931",
        "line": null,
        "message": "The invoice total VAT amount does not match the sum of VAT breakdown amounts. Check that all tax subtotals are consistent with TaxTotal."
      },
      {
        "rule": "PEPPOL-EN16931-R004",
        "layer": "peppol-bis",
        "line": null,
        "message": "The InvoiceTypeCode is not 380. Peppol BIS 3.0 requires type code 380 for standard invoices."
      }
    ]
  }
}

Enter fullscreen mode Exit fullscreen mode

The layer field is worth highlighting, it tells you whether a violation is a fundamental EN 16931 data problem or a Peppol network-specific configuration issue. That distinction matters operationally: a layer: en16931 error means the invoice data is wrong; a layer: peppol-bis error means the invoice data might be fine but it will be rejected by the Peppol network specifically. Different root causes, different teams to involve.

Both valid and invalid invoices return HTTP 200. Branch on the valid boolean, not on HTTP status code. This keeps the integration clean, no exception handling for the expected invalid case.

No XSLT processor in your project. No IKVM. No Java type interop. No Schematron files to embed and update quarterly. No SVRL parsing layer to write and maintain. No javax.xml in your C# codebase.

The same endpoint handles ZUGFeRD and Factur-X PDFs (CII is extracted from the PDF/A-3b container automatically), XRechnung (CIUS-REC-DE rules applied on top of EN 16931), and CII XML, all from the same HTTP call pattern, all returning the same response structure.


The broader point

The IKVM/Saxon path is the only community answer to a real gap in the .NET ecosystem. For a proof of concept, a local testing tool, or a one-off conversion script, it is functional. For a production application where invoice validation determines whether legally significant financial documents are accepted or rejected, the maintenance overhead, four-way version compatibility, quarterly Schematron updates across multiple formats, SVRL parsing, Java type leakage, is a recurring cost that is very easy to underestimate at the "install the NuGet package" stage.

If you are adding e-invoicing validation to a .NET application and the compliance surface is growing, Belgium's mandate is live, France follows September 2026, Germany's issuing obligation hits January 2027, it is worth thinking honestly about whether owning that maintenance is the right use of your team's time.


Resources


I'm Stefan, founder of InvoiceXML - a REST API for Peppol and EN 16931 e-invoice compliance covering Peppol UBL, ZUGFeRD, Factur-X, XRechnung, and CII. Happy to answer questions in the comments.