C# / .NET bindings¶
The Fatoority NuGet package wraps the Rust SDK through Diplomat-generated C#
classes in namespace Fatoora. It targets .NET 8 or later and includes the native
fatoora_ffi library for the packaged runtime identifiers (RIDs).
Build and install¶
From a source checkout with Rust, Python 3.11+ and the .NET 8 SDK:
python3 scripts/package_dotnet.py --output-dir dist/dotnet
In your application's project directory, install the local package:
dotnet add package Fatoority --version 0.4.2 --source /absolute/path/to/dist/dotnet
A local build includes the host RID. The C# CI workflow tests Linux, Windows and
macOS and assembles their native libraries into a combined nuget artifact.
Version tags publish the tested artifact to NuGet.org through trusted publishing;
the commands above also support local packages before publication. Match the application's OS and architecture to a RID included in
the package. NuGet supplies the native asset at runtime, so no library search-path
override is needed.
Release linux-x64 assets are built and tested on glibc 2.28 (AlmaLinux 8).
They require glibc 2.28 or newer; musl/Alpine is not included. Locally built
packages inherit the build host's libc requirements.
Generated sources are checked in. Building the package does not require Diplomat. Regeneration uses a pinned fork containing the runtime safety fixes; see the binding workflow.
Example¶
The guide snippets below are methods from the buildable
example project.
They use Fatoora, System, and System.Text.Json.
public static (string CsrPem, string KeyPem) GenerateCsr(string propertiesText)
{
using var properties = CsrProperties.FromPropertiesStr(propertiesText);
using var key = SigningKey.Generate();
using var csr = properties.Build(key, 0); // Nonproduction.
return (csr.ToPem(), key.ToPem());
}
Use environment 0 for nonproduction, 1 for simulation, and 2 for production.
Configuration, CSR generation, and credentials must agree on the environment.
See the CSR, signing,
validation, and QR guides for
C# examples alongside the other languages.
Values and ownership¶
| Rust bridge value | C# representation |
|---|---|
| Text input | string; invalid UTF-16 and embedded NUL are rejected |
| Optional text input | string?; null and "" remain distinct |
| Optional text output | Text?; read Value() and dispose the owner when present |
| Decimal amount | Invariant decimal string; use CultureInfo.InvariantCulture for conversion |
| Invoice counter | ulong |
| Owned native object | An IDisposable class; use using or call Dispose() |
| Owned binary output | Bytes; call AsSlice() and copy with Clone() when needed |
| Invoice acceptance | InvoiceOutcome.Accepted, Rejected, or Unknown |
All 31 opaque classes implement IDisposable, including returned snapshots and
BindingError. Disposal is idempotent; later calls on that wrapper throw
ObjectDisposedException. Finalization provides cleanup if disposal is omitted,
but its timing is not deterministic. Releasing an owner does not guarantee
zeroization of secrets or copies exported to managed strings and arrays.
InvoiceBuilder.Build(), Signer.Sign(), and SignedInvoice.IntoXml() consume
inner state. The outer wrapper remains disposable, but reusing consumed state
raises BindingErrorException. Build and signing failures can also consume
state. Returned invoice data, totals, addresses, and response children are owned
snapshots and remain usable after their parents are disposed.
For binary output, dispose both the owner and its borrowed view:
using Fatoora;
using var key = SigningKey.Generate();
using var bytes = key.ToDer();
using var view = bytes.AsSlice();
byte[] der = view.Clone(); // Independent managed copy.
WithSpan() provides a scoped, read-only view without copying. An active read
retains native memory through concurrent source disposal. Later reads fail after
the source is disposed. Disposing the view while a read is active throws
InvalidOperationException; retry once the read has finished. Conflicting calls
that require mutable access also throw InvalidOperationException.
Errors¶
public static void ShowError()
{
try
{
using var key = SigningKey.FromPem("invalid key");
}
catch (BindingErrorException error)
{
using var details = error.Inner;
Console.WriteLine($"{details.Code()}: {error.Message}");
Console.WriteLine(details.DetailsJson());
}
}
BindingErrorException.Message contains the native message. Inner.Code() uses
the shared error codes, and Inner.DetailsJson() preserves
structured details such as HTTP status and response body. Dispose Inner after
reading it. Invalid UTF-16 fails before the native call with
EncoderFallbackException; invalid native inputs, including embedded NUL, use
BindingErrorException.
API requests¶
public static void ReportInvoice(string signedXml, string token, string secret)
{
using var config = Config.New(2); // Production.
using var client = ZatcaClient.Create(config);
using var credentials = CsidProduction.Create(2, null, token, secret);
using var invoice = SignedInvoice.FromXml(signedXml);
using var response = client.ReportSimplifiedInvoice(
invoice, credentials, false, "en");
response.EnsureAccepted(); // HTTP success alone does not mean acceptance.
Console.WriteLine(response.Outcome());
}
// This method performs a blocking network request.
The native client methods block the calling thread. HTTP success alone does not
establish invoice acceptance: inspect Outcome() or call EnsureAccepted().
For clearance, read and save ClearedInvoiceXml() when present. See the
API guide for rejection,
unknown outcomes, and cleared-document semantics.
| Blocking method | Prepared equivalent | Credential scope / result |
|---|---|---|
PostCsrForCcsid |
PreparePostCsrForCcsid |
CSR and OTP to compliance credentials |
PostCcsidForPcsid |
PreparePostCcsidForPcsid |
Compliance to production credentials |
RenewCsid |
PrepareRenewCsid |
Production credentials, CSR and OTP to renewed credentials |
CheckInvoiceCompliance |
PrepareCheckInvoiceCompliance |
Compliance credentials; validation response |
ReportSimplifiedInvoice |
PrepareReportSimplifiedInvoice |
Production credentials; reporting response |
ClearStandardInvoice |
PrepareClearStandardInvoice |
Production credentials; clearance response |
For asynchronous HTTP or cancellation, use a Prepare* method and your own
HttpClient. The request exposes Method(), Url(), HeadersJson() (a JSON
array of header pairs), and an owned Body(). Copy its bytes before awaiting the
transport. Pass the HTTP status and response bytes to Decode(ushort, byte[]),
including non-2xx responses, so Rust can classify gateway errors. Configure the
transport to refuse redirects, and pass your cancellation token to send/read
operations. The generated blocking methods do not accept a cancellation token.
Prepared requests own their wire data, credentials, and decoding context; keep the request alive until decoding completes. Headers contain credentials and should not be logged.
Local validation¶
Xml.ValidateZatca(config, xml, optionsJson) returns a JSON report. Inspect
is_valid and stages; a rejected document returns a report, while an execution
failure throws an exception that may carry a partial report in its details.
Provide the predecessor hash from invoice history and set evaluated_at for
repeatable checks. Omitting the timestamp uses the native clock.
All bundled UBL schemas are embedded. Installed packages do not need the source checkout, Java, or an external SDK installation. Local validation does not establish gateway acceptance or certificate issuer trust. See the validation reference.
Licensing¶
The native library includes SDK-derived rules and messages subject to the upstream terms in the bundled third-party notices. Those terms apply to the native component distributed with the C# package, as they do to other SDK distributions.
Verification¶
python3 scripts/check_dotnet.py
python3 scripts/package_dotnet.py --output-dir dist/dotnet
python3 scripts/check_dotnet.py --package-only --package-dir dist/dotnet
The checks compile the guide examples and cover Unicode conversion, optional strings, errors, disposal, concurrent byte access, key encodings, signing, frozen SDK hashes, prepared requests, and response snapshots. Package verification uses a fresh consumer with no project reference or native search-path overrides. The CI matrix tests the combined NuGet package on Linux, Windows, and macOS. Local Linux results do not establish Windows/macOS or live gateway acceptance.