XmlAuditfile 0.0.7
dotnet add package XmlAuditfile --version 0.0.7
NuGet\Install-Package XmlAuditfile -Version 0.0.7
<PackageReference Include="XmlAuditfile" Version="0.0.7" />
<PackageVersion Include="XmlAuditfile" Version="0.0.7" />
<PackageReference Include="XmlAuditfile" />
paket add XmlAuditfile --version 0.0.7
#r "nuget: XmlAuditfile, 0.0.7"
#:package XmlAuditfile@0.0.7
#addin nuget:?package=XmlAuditfile&version=0.0.7
#tool nuget:?package=XmlAuditfile&version=0.0.7
auditfile-dotnet
Streaming reader for Dutch tax audit files (XAF and related), following auditfile-spec.
Status: draft (0.x). Not affiliated with the Belastingdienst or any software vendor.
Packages
| Package | Contents |
|---|---|
XmlAuditfile |
The reader: version resolution, validation, ISink, row streaming. No dependencies. |
XmlAuditfile.Memory |
MemorySink: keeps all rows per table in memory. Depends on the reader. |
Write your own ISink to load into a database or anything else; only the reader package is needed then.
using XmlAuditfile;
using XmlAuditfile.Memory;
var sink = new MemorySink();
var source = AuditfileSource.FromFactory(() => File.OpenRead("administratie.xaf"), "administratie.xaf");
var report = await Auditfile.ReadAsync(source, sink);
foreach (var diagnostic in report.Diagnostics)
{
Console.WriteLine(diagnostic);
}
foreach (var line in sink.Rows("tr_line"))
{
Console.WriteLine($"{line.GetString("acc_id")} {line.GetDecimal("amnt")} {line.GetString("amnt_tp")}");
}
Rows have typed accessors (GetString, GetDecimal, GetInteger, GetDate, GetDateTime, GetTime, GetBoolean) that check
the column type and name the problem; TryGetValue tells whether a column exists in this version. GetDecimal also reads an
integer column, since a column can be integer in one version and decimal in another. In a loop over many rows, look the
column up once (table.FindColumn("amnt")) and pass the SchemaColumn: no lookup by name per row.
Values are read in XSD notation only: a date-time such as 03/01/2017 10:00:00 is refused (XAF-S004) rather than guessed.
What it does
- Determines the version: forced in code, official namespace or alias from the registry, optionally
derived from the namespace (
UseResolver). Anything else stops with a clear diagnostic. - Reads in two phases: scan (validate against the built-in XSD, count rows per table) and load (stream rows in batches to a sink). Verified mode loads only a valid file.
- Turns every repeating element into a table following the manifest generated by auditfile-spec.
Rows know their id, parent, position and source part; values are typed (
decimal,long,DateTime, ...). - Checks required and expected tables, keys, references and the XAF business rules (balanced transactions, header counts and totals).
- Reads split audit files (
audit.xaf,audit-2.xaf, ...) as one file; you pass the parts in order. - Reports everything as diagnostics with stable codes, in Dutch or English.
Entry points
| Call | Use |
|---|---|
Auditfile.ReadAsync(source, sink, options) |
Read into a sink: MemorySink (Memory package) or your own ISink |
Auditfile.ValidateAsync(source, options) |
Validate without storing anything; Mode = Unverified skips the XSD |
Auditfile.Scan(source, options) |
Version, XSD validation and row counts only |
Auditfile.Open(source, options).ReadRows() |
Stream rows yourself; the rows can be enumerated once |
Every call takes a CancellationToken, checked in the scan phase as well as the load phase.
ReadOptions refuses values out of range (BatchSize, MaxDiagnostics, MaxDepth, MaxValueLength at least 1)
with an ArgumentOutOfRangeException. When a sink fails and its AbortAsync fails too, the read throws an
AggregateException with both errors.
| AuditfileVersion.Financial.V3_2 | Manifest, XSD, definitions and dialects of a version |
| Auditfile.Dialects.Add(version, json) / Auditfile.Views.Add(json) | Add your own dialects and views next to the built-in ones (see below) |
| AuditfileSchema.ToJson() / FromJson() | Store and restore the schema of an import |
Sources: the reader never opens files; you supply the streams.
| Call | Use |
|---|---|
AuditfileSource.FromFactory(open, name) |
Fresh stream per call: can be read twice (scan, then load) |
AuditfileSource.FromFactories((name, open), ...) |
Same, for the parts of a split audit file |
AuditfileSource.FromStream(stream, name) |
Read once; validation then happens during loading |
AuditfileSource.FromStreams((name, stream), ...) |
Same, for the parts of a split audit file |
// Files on disk, e.g. audit.xaf and audit-2.xaf
var source = AuditfileSource.FromFactories(paths.Select(p => (Path.GetFileName(p), (Func<Stream>)(() => File.OpenRead(p)))).ToArray());
Custom dialects and views
Dialects and views come from auditfile-spec and are embedded in the package. You can add your own at runtime, in the same
format as dialects.json and views/<type>.json of the spec, without a fork or a custom build. Pass the JSON text or
a UTF-8 stream; the reader opens no files, so open them yourself.
// { "formatVersion": 1, "dialects": [ { "name": "my-package", "match": { ... }, "corrections": [ ... ] } ] }
Auditfile.Dialects.Add(AuditfileVersion.Financial.V3_2, File.ReadAllText("dialects.json"));
// { "formatVersion": 1, "type": "xaf", "views": [ { "name": "lines_per_account", ... } ] }
using (var views = File.OpenRead("views.json"))
{
Auditfile.Views.Add(views);
}
var report = await Auditfile.ReadAsync(source, sink); // recognizes and applies the added dialect
var result = sink.Run(sink.Schema!.Version!.FindView("lines_per_account")!);
- Added definitions come after the built-in ones in
AuditfileVersion.DialectsandViews, soFindView, version resolution and dialect corrections use them like the built-in ones. A view is added for every version of itstype; whether it fits a version is decided when it runs (XAF-Q001), as for built-in views.ApplyDialects = falseignores added dialects too. - A file is checked when it is added and refused as a whole when anything is wrong (
FormatException): invalid JSON, aformatVersionother than 1, an unknown dialect action, view aggregate or operator, or a correctioncodethat is not a dialect diagnostic (XAF-C001–C003). An unknown audit file type or a name the version or type already has throws anArgumentException. Built-in definitions cannot be replaced or removed. Auditfile.Dialects.Remove(version, name)andAuditfile.Views.Remove(type, name)remove one added definition;Clear()removes all of them. The registries are thread-safe and shared by every read in the process; a read that has started keeps the dialects it found.
Targets
netstandard2.0 (.NET Framework 4.7.2+) and net10.0. No package dependencies on any target.
On net10.0 the library is trimming and Native AOT compatible; the trim and AOT analyzers run in every build.
Example programs live in auditfile-dotnet-samples.
WebAssembly: the library uses no file system, threads or reflection; all input comes from your streams. Browser file streams are
asynchronous only, so copy the file into memory first and use
AuditfileSource.FromFactory(() => new MemoryStream(bytes)).
Web applications: the XML is parsed synchronously; only the sink is called asynchronously. Parsing is CPU-bound,
and asynchronous reads per node would only slow it down. ASP.NET Core does not allow synchronous reads of the request
body, so do not pass Request.Body to FromStream: copy the upload to a temporary file and read that. A large file keeps a
thread busy while it is read, so run big imports in a background job rather than in the request.
var path = Path.GetTempFileName();
try
{
await using (var file = File.Create(path))
{
await upload.CopyToAsync(file, cancellationToken); // upload: IFormFile
}
var report = await Auditfile.ValidateAsync(
AuditfileSource.FromFactory(() => File.OpenRead(path), upload.FileName), cancellationToken: cancellationToken);
}
finally
{
File.Delete(path);
}
Diagnostics
Every finding is a diagnostic with a stable code from diagnostics.json of auditfile-spec. The report lists at most
MaxDiagnostics (default 1000), followed by XAF-X900 when more were found. A full list makes room for errors by
leaving out infos and warnings, and fatal diagnostics are always included. IsValid, HasErrors, HasFatal and
Count(severity) count every diagnostic, also the ones left out; IsTruncated says whether some were.
| Layer | Codes this reader reports |
|---|---|
| XML | X001–X004 (X004 also for a part of a split file in another encoding than the first part), X900 |
| Version | V001–V006 |
| Structure | S001–S005, S008; S006 in unverified mode (in verified mode the XSD reports a missing field as D001) |
| XSD | D001, D002 |
| Rules | R001–R004, R017 |
| Dialect | C001–C003, when a dialect applies |
| Views | Q001, Q002 |
Defined in the spec but not checked yet: S007 and S009 (in verified mode the XSD reports too many occurrences and values outside a code list as D001), R005–R016 (ascending numbers, periods, eleven test, IBAN, personal data), X005 (the XML parser reports an invalid character as X001) and E001–E004 (envelopes).
Layout
| Path | Contents |
|---|---|
src/XmlAuditfile/ |
The reader |
src/XmlAuditfile.Memory/ |
MemorySink |
tests/XmlAuditfile.Tests/ |
Unit tests, using the XAF 3.2 sample in Data/xaf-3.2-sample.xaf (two intended findings: XAF-R001, XAF-R003) |
spec.lock.json |
The pinned auditfile-spec release |
tools/ |
fetch-spec.ps1 and the semver helpers used by the workflows |
Definitions
Everything in generated/ of a pinned auditfile-spec release (XSD, manifests, definitions,
dialects, registry, diagnostics) is embedded in the package. The version objects (AuditfileVersion)
are part of this reader; a test checks that they match the registry.
The spec is not committed. spec.lock.json pins a release (tag and commit) of
auditfile-spec, and tools/fetch-spec.ps1 copies only
generated/ of that commit into spec/generated/ (git-ignored; a plain folder, not a git checkout). The build stops with a clear error when spec/ is missing.
pwsh tools/fetch-spec.ps1 # after cloning and whenever spec.lock.json changes
dotnet build XmlAuditfile.slnx
dotnet test --solution XmlAuditfile.slnx
To build against a local spec checkout instead: dotnet build -p:SpecRoot=../auditfile-spec/.
Workflows
| Workflow | What it does |
|---|---|
ci.yml |
Push to main and pull requests: fetch the pinned spec, build, test |
release.yml |
Manual, with a version such as 1.0.1. From main the version must be higher than every release; from release/X.Y (maintenance of an older line, e.g. a security fix 0.1.1 after 1.0.0, branched off tag 0.1.0) it must be in line X.Y and higher than the latest release in that line. Then it fetches the pinned spec, builds, tests, packs, creates the tag and GitHub release, and publishes the packages and symbols to NuGet through Trusted Publishing when the NUGET_USER variable is set (environment nuget) |
spec-update.yml |
Weekly: when auditfile-spec has a higher release than spec.lock.json, updates the lock, builds and tests, and opens a pull request with the result |
dependabot.yml |
Weekly updates of NuGet packages and GitHub Actions |
License
Apache License 2.0.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
-
net10.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on XmlAuditfile:
| Package | Downloads |
|---|---|
|
XmlAuditfile.Memory
In-memory sink for XmlAuditfile: keeps all rows per table. Useful for tests, small files and the browser. |
GitHub repositories
This package is not used by any popular GitHub repositories.