Pain.Net
1.0.0
See the version list below for details.
dotnet add package Pain.Net --version 1.0.0
NuGet\Install-Package Pain.Net -Version 1.0.0
<PackageReference Include="Pain.Net" Version="1.0.0" />
<PackageVersion Include="Pain.Net" Version="1.0.0" />
<PackageReference Include="Pain.Net" />
paket add Pain.Net --version 1.0.0
#r "nuget: Pain.Net, 1.0.0"
#:package Pain.Net@1.0.0
#addin nuget:?package=Pain.Net&version=1.0.0
#tool nuget:?package=Pain.Net&version=1.0.0
Pain.Net
Maintained, tested, zero-dependency ISO 20022 SEPA payment-initiation for .NET. Generate and parse pain.001.001.09 (customer credit transfer) and pain.008.001.08 (customer direct debit), with NbOfTxs and CtrlSum computed for you on write and re-validated on read.
MIT Zero dependencies Native AOT clean
Why
The long-time incumbent for SEPA XML in .NET, SepaWriter, has been abandoned since 2021. It is write-only, .NET Framework era, and no longer maintained. If you need to emit a payment file today you either fork a dead package or hand-roll System.Xml, and if you need to read a file back you are on your own.
Pain.Net is the maintained replacement: a small, tested, zero-dependency library that both writes and reads the two SEPA initiation messages, using only the in-box System.Xml.Linq. No transitive dependencies, Native-AOT clean, decimal money throughout.
Install
dotnet add package Pain.Net
Write a credit transfer (pain.001.001.09)
using PainNet;
var transfer = new CreditTransfer(
MessageId: "MSG-001",
CreationDateTime: new DateTimeOffset(2024, 3, 1, 9, 30, 0, TimeSpan.Zero),
Debtor: new Party("Acme GmbH"),
DebtorIban: "DE89370400440532013000",
DebtorBic: "COBADEFFXXX",
Transactions: new[]
{
new CreditTransferTransaction("E2E-1", 1234.56m, "EUR",
new Party("Beta SARL"), "FR7630006000011234567890189", "BNPAFRPPXXX", "Invoice 1"),
new CreditTransferTransaction("E2E-2", 78.90m, "EUR",
new Party("Gamma BV"), "NL91ABNA0417164300", null, null),
});
string xml = Pain.WriteCreditTransfer(transfer);
Write a direct debit (pain.008.001.08)
var debit = new DirectDebit(
MessageId: "MSG-DD-001",
CreationDateTime: new DateTimeOffset(2024, 3, 1, 9, 30, 0, TimeSpan.Zero),
Creditor: new Party("Utility Co"),
CreditorIban: "DE89370400440532013000",
CreditorBic: "COBADEFFXXX",
CreditorSchemeId: "DE98ZZZ09999999999",
Transactions: new[]
{
new DirectDebitTransaction("DD-1", 42.00m, "EUR",
new Party("Customer One"), "FR7630006000011234567890189", "BNPAFRPPXXX",
MandateId: "MANDATE-1", MandateSignatureDate: new DateOnly(2023, 1, 10),
RemittanceInfo: "Subscription"),
});
string xml = Pain.WriteDirectDebit(debit);
Read one back
CreditTransfer parsed = Pain.ReadCreditTransfer(xml);
// Or non-throwing:
if (Pain.TryReadCreditTransfer(xml, out CreditTransfer? maybe))
{
// maybe is populated and validated
}
The NbOfTxs / CtrlSum guarantee
Two fields cause more rejected SEPA files than any other: the transaction count (NbOfTxs) and the control sum (CtrlSum). If they disagree with the transactions actually in the file, the bank rejects the whole batch, and the bug is silent until then.
Pain.Net closes that gap from both sides:
- On write,
NbOfTxsandCtrlSumare always computed from the transactions themselves, in both the group header and each payment block. A caller-supplied total is never trusted, because there is no way to supply one. - On read, the stated
NbOfTxsandCtrlSumare recomputed from the parsed transactions and compared. If either disagrees,ReadCreditTransferthrowsPainValidationException(andTryReadCreditTransferreturnsfalse). That mismatch is exactly the bug this catches.
All money is decimal, formatted invariant with two decimals and a Ccy attribute. CreationDateTime is caller-supplied and written as ISO 8601, so output is deterministic and testable.
Correctness
Every claim here is backed by the xUnit suite, which runs in seconds:
- Credit-transfer writer produces one
PmtInf, the right number ofCdtTrfTxInf, the correct IBANs and amounts, andNbOfTxs/CtrlSumequal to the exact decimal sum. - Full round-trip: write then read recovers the message id, transaction count, and per-transaction end-to-end id, amount, and creditor IBAN.
- Tamper tests: corrupting
CtrlSumorNbOfTxsin valid XML makes the reader throwPainValidationException. - Direct-debit writer emits the right totals, mandate ids, signature dates, and SEPA
SEPA/COREcodes. - Guard rails: empty
MessageId, emptyEndToEndId, emptyMandateId, or a non-positive amount throwArgumentException. - Structural: the root is
Documentin thepain.001.001.09namespace URN with aCstmrCdtTrfInitnchild.
Notes and limitations
- Supports
pain.001.001.09andpain.008.001.08only, on the SEPA profile (SvcLvlSEPA, direct debitsCORE/RCUR, charge bearerSLEV). - The agents that SEPA makes mandatory are required and always emitted: the debtor and creditor BICs (
DbtrAgt/CdtrAgt), and for direct debits the creditor identifier (CreditorSchemeId, written asCdtrSchmeId). Empty values are rejected withArgumentException. - IBAN, BIC, and creditor identifier are passed through as strings and are not format-validated here beyond the non-empty check. Validate their structure with a dedicated library before serializing.
- Amounts must have at most two decimal places; more are rejected with
ArgumentException. Money isdecimalthroughout, formatted invariant with two decimals. CreationDateTimeis written with its UTC offset (yyyy-MM-ddTHH:mm:sszzz) and round-trips exactly; the reader also accepts the offset-less form.- The reader is provided for the credit-transfer message (
ReadCreditTransfer/TryReadCreditTransfer), and validates both group-level and per-PmtInfNbOfTxs/CtrlSum; the direct-debit side is write-only in this release. - Other optional SEPA elements not modelled include ultimate parties and batch-booking flags. Add them if your bank requires them.
- XML is produced schema-shaped in the correct element order with an explicit UTF-8 declaration and no byte-order mark; it is not validated against the official XSD at runtime.
License
MIT. Copyright Israel Iyonsi.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. 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 was computed. 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. |
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.