JAK.DocGen.Client
0.3.0
dotnet add package JAK.DocGen.Client --version 0.3.0
NuGet\Install-Package JAK.DocGen.Client -Version 0.3.0
<PackageReference Include="JAK.DocGen.Client" Version="0.3.0" />
<PackageVersion Include="JAK.DocGen.Client" Version="0.3.0" />
<PackageReference Include="JAK.DocGen.Client" />
paket add JAK.DocGen.Client --version 0.3.0
#r "nuget: JAK.DocGen.Client, 0.3.0"
#:package JAK.DocGen.Client@0.3.0
#addin nuget:?package=JAK.DocGen.Client&version=0.3.0
#tool nuget:?package=JAK.DocGen.Client&version=0.3.0
JAK.DocGen.Client
Client .NET officiel du service JAK.DocGen. Il permet de construire des documents PDF à partir de blocs sémantiques, de résoudre un thème depuis une charte graphique, de générer un PDF et d'obtenir l'aperçu PNG d'une page.
La bibliothèque ne calcule aucun montant et ne contient aucune mise en page. L'application appelante fournit les données métier dans leur ordre d'affichage ; le service applique le thème, les règles de coupure, les en-têtes, les pieds de page et la pagination.
Sommaire
- Installation et compatibilité
- Configuration
- Démarrage rapide
- Cycle de vie d'un thème
- Construire un document
- Catalogue des blocs
- Libellés et locales
- Générer un PDF ou un aperçu
- Erreurs et annulation
- Tests
- Règles d'intégration
Installation et compatibilité
Le package cible netstandard2.0 et net8.0. Il est utilisable notamment depuis .NET 8+ et
.NET Framework 4.6.2+.
Depuis NuGet :
dotnet add package JAK.DocGen.Client --version 0.3.0
Depuis ce dépôt, pendant le développement :
<ItemGroup>
<ProjectReference Include="../JAK.DocGen/clients/dotnet/JAK.DocGen.Client/JAK.DocGen.Client.csproj" />
</ItemGroup>
Tous les types publics se trouvent dans le namespace suivant :
using Jak.DocGen;
Configuration
ASP.NET Core et Generic Host
Enregistrez le client une fois dans Program.cs :
using Microsoft.Extensions.DependencyInjection;
builder.Services.AddDocGenClient(options =>
{
options.BaseAddress = new Uri(builder.Configuration["DocGen:BaseAddress"]!);
options.ApiKey = builder.Configuration["DocGen:ApiKey"]!;
});
Exemple de configuration non sensible :
{
"DocGen": {
"BaseAddress": "http://docgen:8090"
}
}
Conservez la clé dans un secret, une variable d'environnement ou un coffre de secrets. Ne la
committez pas dans appsettings.json.
AddDocGenClient enregistre IDocGenClient et DocGenClient en singleton. Le HttpClient
sous-jacent reste géré par IHttpClientFactory. La méthode retourne un IHttpClientBuilder, ce
qui permet de fixer le délai HTTP sans créer un second client :
builder.Services
.AddDocGenClient(options =>
{
options.BaseAddress = new Uri(builder.Configuration["DocGen:BaseAddress"]!);
options.ApiKey = builder.Configuration["DocGen:ApiKey"]!;
})
.ConfigureHttpClient(http => http.Timeout = TimeSpan.FromSeconds(45));
Injectez ensuite l'interface :
public sealed class InvoicePdfService(IDocGenClient docGen)
{
private readonly IDocGenClient _docGen = docGen;
}
Une adresse ou une clé absente provoque une ArgumentException lors de la première résolution
du client par le conteneur. L'erreur nomme l'option manquante.
Construction directe
Pour une application console ou une application sans conteneur DI :
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(45) };
var client = new DocGenClient(http, new DocGenClientOptions
{
BaseAddress = new Uri("http://docgen:8090"),
ApiKey = Environment.GetEnvironmentVariable("DOCGEN_API_KEY")
?? throw new InvalidOperationException("DOCGEN_API_KEY manque."),
});
Dans ce mode, la durée de vie du HttpClient appartient à l'appelant. DocGenClient ne le
dispose jamais.
Démarrage rapide
Le flux normal comporte deux étapes : résoudre le thème lors de la sauvegarde d'une charte, puis réutiliser le thème résolu pour chaque rendu.
using Jak.DocGen;
// À la création ou à la modification de la charte.
ThemeResolution resolution = await client.ResolveThemeAsync(theme => theme
.WithBrand("#1b4965")
.WithBodyFont("Liberation Sans", size: "9.5pt")
.WithDensity(ThemeDensity.Comfortable));
ResolvedTheme theme = resolution.Theme;
// À chaque génération de document.
DocumentRequest request = DocumentBuilder.ForLocale(DocGenLocale.FrBE)
.WithTheme(theme)
.AddTitle(title => title
.Text("Offre 2026-001")
.Reference("2026-001")
.Date(new DateOnly(2026, 8, 20)))
.AddParties(parties => parties
.Issuer(issuer => issuer
.Name("Société Exemple SRL")
.Address(["Rue de l'Exemple 1"], "1000", "Bruxelles")
.VatNumber("BE 0123.456.789"))
.Recipient(recipient => recipient
.Name("Destinataire Exemple SA")
.Address(["Avenue de la Démonstration 2"], "5000", "Namur")))
.AddLineItems(items => items
.Currency("EUR")
.Columns(
LineItemColumn.Description,
LineItemColumn.Quantity,
LineItemColumn.UnitPrice,
LineItemColumn.Total)
.Section("Prestations", section => section
.Line(
"Analyse",
total: 1250m,
quantity: 1m,
unit: "forfait",
unitPrice: 1250m)
.Subtotal(1250m))
.Totals(
exclVat: 1250m,
inclVat: 1512.50m,
(Rate: 21m, Base: 1250m, Amount: 262.50m)))
.AddSignature(signature => signature
.Place("Bruxelles")
.Date(new DateOnly(2026, 8, 20))
.Signatory("Alex Exemple", role: "Administrateur"))
.Build();
RenderedDocument result = await client.RenderAsync(request);
await File.WriteAllBytesAsync("offre.pdf", result.Pdf);
DateOnly est disponible sur la target net8.0. Sur netstandard2.0, utilisez les surcharges
DateTime. Les deux sont sérialisées au format stable yyyy-MM-dd, indépendamment de la culture
et du fuseau horaire du processus.
Cycle de vie d'un thème
Résoudre les primitives
Le builder de thème accepte uniquement les choix de charte que le service sait dériver :
ThemeResolution resolution = await client.ResolveThemeAsync(theme => theme
.WithBrand("#1b4965", neutral: "#5b5b5b", alert: "#c92a2a")
.WithBodyFont("Liberation Sans", size: "9.5pt")
.WithHeadingFont("Liberation Sans", weight: 700)
.WithDensity(ThemeDensity.Compact)
.WithPage(PageSize.A4, PageOrientation.Portrait)
.WithLogo(
DataUri.Svg(svgLogo),
maxWidth: "14mm",
position: LogoPosition.HeaderLeft));
Référence de ThemeBuilder :
| Méthode | Effet exact |
|---|---|
Create() |
crée un builder vide ; le service utilisera ses valeurs neutres pour tout ce qui reste absent |
ForBrand(color) |
crée un builder et définit immédiatement la couleur principale de la charte |
WithBrand(brand, neutral, alert) |
définit la couleur principale obligatoire et, si fournies, une couleur neutre et une couleur d'alerte |
WithBodyFont(family, size) |
choisit la police du texte courant et éventuellement sa taille en pt |
WithHeadingFont(family, weight) |
choisit la police des titres et éventuellement sa graisse de 100 à 900 |
WithDensity(density) |
choisit l'espacement général Comfortable ou Compact |
WithPage(size, orientation) |
définit le format et l'orientation utilisés par défaut par les documents de cette charte |
WithLogo(dataUri, maxWidth, position) |
ajoute le logo embarqué dans le thème, sa largeur maximale d'impression et sa position dans l'en-tête |
Build() |
produit les ThemePrimitives à transmettre à ResolveThemeAsync ; cette méthode ne résout pas elle-même le thème |
Les unités de taille acceptées par le service sont mm et pt. Une police doit exister dans
le catalogue du service et être autorisée pour l'embarquement PDF. Les URLs d'image sont
interdites : logo et signature voyagent en Data URI.
Pour le cas courant, une couleur suffit :
ThemeResolution resolution = await client.ResolveThemeAsync(
ThemePrimitives.ForBrand("#1b4965"));
ThemeResolution.Warnings contient les avertissements non bloquants, par exemple un contraste
faible :
foreach (ThemeIssue warning in resolution.Warnings)
{
logger.LogWarning(
"Thème DocGen : {Code} sur {Path} — {Message}",
warning.Code,
warning.Path,
warning.Message);
}
Persister et recharger le thème résolu
Le service est sans état. L'application appelante persiste le thème complet retourné par
ResolveThemeAsync, puis le recharge sans le remodeler :
string jsonToStore = resolution.Theme.ToJson();
// Plus tard, depuis la valeur stockée par l'application appelante.
ResolvedTheme storedTheme = ResolvedTheme.FromJson(jsonFromStorage);
Ne rappelez pas ResolveThemeAsync à chaque document. Rappelez-le uniquement quand la charte
change ou doit être migrée vers une nouvelle version de schéma.
Construire un document
Un document est une liste ordonnée de blocs : l'ordre des appels Add... est l'ordre de rendu.
Le builder exige un thème et au moins un bloc.
Référence de DocumentBuilder :
| Méthode | Obligatoire | Effet exact |
|---|---|---|
ForLocale(locale) |
oui | démarre un document en fr-BE ou nl-BE ; la locale pilote les libellés par défaut et le format des nombres et dates |
WithTheme(theme) |
oui | associe au document un thème complet préalablement résolu par le service |
WithWatermark(watermark) |
non | ajoute ou retire le filigrane prédéfini Draft, Duplicate ou None |
WithPageSetup(size, orientation) |
non | surcharge, pour ce document seulement, le format et l'orientation définis dans le thème |
OverrideLabels(configure) |
non | remplace certains libellés mécaniques du service sans modifier les données ni la mise en page |
AddTitle(...) |
non | ajoute un bandeau de titre répété en haut de chaque page ; un second appel ouvre une nouvelle section, sur page neuve, avec son propre bandeau |
AddParties(...) |
non | ajoute les fiches de l'émetteur et du destinataire |
AddKeyValues(...) |
non | ajoute une liste de propriétés sous forme de couples libellé/valeur |
AddRichText(...) |
non | ajoute une section de texte composée d'un titre de section optionnel et de paragraphes |
AddLineItems(...) |
non | ajoute un tableau de lignes chiffrées, ses sections et ses totaux déjà calculés |
AddPaymentSchedule(...) |
non | ajoute un tableau d'échéances de paiement |
AddSignature(...) |
non | ajoute une zone de signature pour un ou deux signataires |
AddLegalTerms(...) |
non | ajoute des conditions générales complètes sous forme de sections de texte |
AddLegalTermsLink(...) |
non | ajoute une référence imprimée et cliquable vers des conditions générales en ligne |
AddSpacer(size) |
non | ajoute un espacement vertical prédéfini entre deux blocs |
AddPageBreak() |
non | force le bloc suivant à commencer sur une nouvelle page |
Build() |
oui, en dernier | vérifie la présence du thème et d'au moins un bloc, puis produit le DocumentRequest |
DocumentBuilder builder = DocumentBuilder.ForLocale(DocGenLocale.NlBE)
.WithTheme(theme)
.WithPageSetup(PageSize.A4, PageOrientation.Landscape)
.WithWatermark(Watermark.Draft);
DocumentRequest request = builder
.AddRichText(text => text.Paragraph("Inhoud van het document."))
.Build();
WithPageSetup surcharge le format du thème pour ce document uniquement. Les filigranes
disponibles sont None, Draft et Duplicate.
Le builder valide immédiatement les champs structurels évidents : titre vide, thème absent, bloc sans élément obligatoire, etc. La validation exhaustive du contrat reste effectuée par le service.
Catalogue des blocs
La version 0.3.0 expose les blocs suivants :
| Bloc ajouté | Ce qui apparaît dans le document | Minimum requis |
|---|---|---|
AddTitle |
bandeau de titre répété sur les pages de sa section ; chaque titre ouvre une section | Text |
AddParties |
deux fiches d'identité : émetteur et destinataire | Issuer, Recipient et leur Name |
AddKeyValues |
liste de propriétés comme « Projet : Rénovation » | au moins un Item |
AddRichText |
section de texte courant ; Heading ajoute seulement un titre au-dessus des paragraphes |
au moins un Paragraph |
AddLineItems |
tableau de prestations ou produits avec montants et totaux | Currency, Columns, une Section et Totals |
AddPaymentSchedule |
tableau des acomptes, échéances ou tranches à payer | Currency et un Installment |
AddSignature |
zone réservée aux noms, rôles et signatures | un Signatory |
AddLegalTerms |
conditions générales imprimées intégralement | une Section contenant un paragraphe |
AddLegalTermsLink |
titre éventuel, texte d'introduction et URL cliquable | url |
AddSpacer |
espace vertical prédéfini | une taille |
AddPageBreak |
fin de page immédiate avant le bloc suivant | rien |
Depuis 0.3.0, le client expose également AddAttendance, AddPhotoGrid et AddFormattedText.
Ils nécessitent un moteur incluant ces blocs (évolution du service).
Les blocs existants et le contrat DocumentRequest.Blocks restent compatibles.
Présences, photos et texte formaté
DocumentRequest request = DocumentBuilder.ForLocale(DocGenLocale.FrBE)
.WithTheme(theme)
.AddAttendance(table => table.Heading("Participants")
.Row("Bureau Exemple", personPresent: true, companyPresent: false,
person: "Alex", role: "Conseil"))
.AddFormattedText(text => text.Heading("Compte rendu")
.Section(TextHeadingLevel.Section, new TextRun("Observations"))
.Paragraph(new TextRun("Texte en gras", Bold: true), new TextRun(" et "),
new TextRun("un lien", Href: "https://example.com"))
.List(false, [new TextRun("Premier point")], [new TextRun("Second point")]))
.AddPhotoGrid(grid => grid.Heading("Illustrations").Columns(PhotoGridColumns.Two)
.Photo(DataUri.Jpeg(imageBytes), "Vue générale"))
.Build();
TextRun porte Bold, Italic, Underline, Strike et Href (HTTP, HTTPS ou mailto).
Les espaces entre segments restent significatifs. TextHeadingLevel propose Section (2) et
Subsection (3) ; PhotoGridColumns propose Two, Three et Four. Les photos utilisent des
data URI PNG/JPEG, jamais des URL externes. Aucune limite de grille ne tronque la liste des photos.
Pour composer le texte depuis un parseur, FormattedTextBuilder.Nodes(...) accepte des
FormattedParagraph, FormattedHeading et FormattedList. Ces types dérivent de
FormattedTextNode et leur sérialisation conserve uniquement les clés prévues par le service.
Les listes sont simples, sans imbrication. Le SDK ne parse pas de Markdown et n’accepte pas de HTML/CSS.
Les builders refusent les collections vides et les options hors énumération ; la validation
complète, notamment des images et liens, appartient au service.
Titre répété
Chaque titleBlock ouvre une section et devient le bandeau répété en haut de chaque page de
cette section (service 0.3.0 et plus ; avant, seul le premier titre devenait le bandeau et les
suivants étaient imprimés dans le flux) :
Méthode de TitleBuilder |
Obligatoire | Ce qu'elle affiche |
|---|---|---|
Text(title) |
oui | titre principal du document dans le bandeau |
Subtitle(subtitle) |
non | ligne secondaire sous le titre principal |
Reference(reference) |
non | référence métier du document dans la zone de métadonnées |
Date(date) |
non | date du document dans la zone de métadonnées |
ValidUntil(date) |
non | date limite de validité dans la zone de métadonnées |
Variant(variant) |
non | Standard masque les métadonnées et conserve le titre seul ; WithReference affiche les métadonnées fournies |
.AddTitle(title => title
.Text("Facture 2026-042")
.Subtitle("Travaux de rénovation")
.Reference("2026-042")
.Date(new DateOnly(2026, 8, 20))
.ValidUntil(new DateOnly(2026, 9, 19))
.Variant(TitleVariant.WithReference))
TitleVariant.Standard n'affiche que le titre. Utilisez WithReference pour les métadonnées.
Un second AddTitle ouvre une nouvelle section : elle commence sur une page neuve et porte son
propre bandeau sur toutes ses pages, le bandeau précédent ne s'y répète plus. C'est ainsi qu'un
devis enchaîne l'offre chiffrée et la convention qui la suit :
DocumentRequest request = DocumentBuilder.ForLocale(DocGenLocale.FrBE)
.WithTheme(theme)
.AddTitle(title => title.Text("Devis CSS no. 26-001").Reference("26-001"))
.AddParties(parties => /* ... */)
.AddLineItems(items => /* ... */)
.AddTitle(title => title.Text("Convention CSS").Variant(TitleVariant.Standard))
.AddLegalTerms(terms => /* ... */)
.AddSignature(signature => /* ... */)
.Build();
Aucun AddPageBreak n'est nécessaire avant le second titre ; en poser un ne crée pas de page
blanche. Les blocs placés avant le premier AddTitle restent sous son bandeau, dans la première
section.
Émetteur et destinataire
PartiesBuilder crée les deux fiches ; chaque fiche est configurée avec un PartyBuilder :
| Méthode | Obligatoire | Ce qu'elle affiche |
|---|---|---|
Issuer(...) |
oui | fiche de l'organisation qui émet le document |
Recipient(...) |
oui | fiche de la personne ou organisation destinataire |
Variant(variant) |
non | fiches côte à côte avec SideBySide, ou l'une sous l'autre avec Stacked |
Name(name) |
oui par fiche | nom principal de la partie |
Address(lines, postalCode, city, country) |
non | adresse sur une ou plusieurs lignes, suivie des informations postales fournies |
VatNumber(value) |
non | numéro de TVA |
CompanyNumber(value) |
non | numéro d'entreprise |
Email(value) |
non | adresse e-mail |
Phone(value) |
non | numéro de téléphone |
Iban(value) |
non | numéro de compte IBAN |
Bic(value) |
non | code BIC |
LegalFooter(value) |
non | mention légale de l'émetteur ; prévue pour la fiche Issuer |
.AddParties(parties => parties
.Variant(PartiesVariant.SideBySide)
.Issuer(party => party
.Name("Société Exemple SRL")
.Address(["Rue de l'Exemple 1"], "1000", "Bruxelles", "Belgique")
.VatNumber("BE 0123.456.789")
.CompanyNumber("0123.456.789")
.Email("info@example.test")
.Phone("+32 2 000 00 00")
.Iban("BE00 0000 0000 0000")
.Bic("EXAMPLEBIC")
.LegalFooter("Société Exemple SRL — RPM Bruxelles"))
.Recipient(party => party
.Name("Destinataire Exemple SA")
.Address(["Avenue de la Démonstration 2"], "5000", "Namur")))
LegalFooter est prévu pour l'émetteur. Les variantes sont SideBySide et Stacked.
Paires clé/valeur
Ce bloc sert aux informations qui ne sont ni une adresse, ni un paragraphe : référence de commande, nom de projet, chantier, mission ou échéance contractuelle.
Méthode de KeyValuesBuilder |
Obligatoire | Effet exact |
|---|---|---|
Heading(heading) |
non | ajoute un titre de section au-dessus de la liste |
Item(label, value) |
oui, au moins une fois | ajoute une propriété : le label décrit le champ et value est sa valeur affichée |
Variant(variant) |
non | organise les propriétés en pile, sur deux colonnes ou sur trois colonnes |
.AddKeyValues(values => values
.Heading("Informations")
.Variant(KeyValuesVariant.TwoColumn)
.Item("Projet", "Rénovation du bâtiment A")
.Item("Votre référence", "PO-2026-123"))
Les variantes sont Stacked, TwoColumn et ThreeColumn.
Texte structuré
AddRichText sert à afficher du texte courant : introduction, note, explication ou commentaire.
Ce n'est pas un bloc destiné uniquement à placer un titre.
Méthode de RichTextBuilder |
Obligatoire | Effet exact |
|---|---|---|
Heading(heading) |
non | ajoute un titre de section au-dessus du texte ; il ne remplace pas le titre principal du document |
Paragraph(text) |
oui, au moins une fois | ajoute un paragraphe de texte courant |
Paragraphs(paragraphs) |
non | ajoute plusieurs paragraphes dans leur ordre d'énumération ; équivaut à appeler Paragraph plusieurs fois |
.AddRichText(text => text
.Heading("Introduction")
.Paragraph("Premier paragraphe.")
.Paragraphs(new[]
{
"Deuxième paragraphe.",
"Troisième paragraphe.",
}))
Le contenu est du texte, jamais du HTML ou du Markdown. Le service l'échappe avant rendu.
Lignes chiffrées et totaux
Méthode de LineItemsBuilder |
Obligatoire | Effet exact |
|---|---|---|
Currency(isoCode) |
oui | définit la devise utilisée pour tous les montants du bloc |
Columns(columns) |
oui | choisit les colonnes visibles et leur ordre de gauche à droite |
Section(title, configure) |
oui, au moins une fois | crée un groupe de lignes ; title peut être nul pour un groupe sans en-tête |
Totals(exclVat, inclVat, vat) |
oui | fournit les totaux hors TVA, TVA par taux et TVA comprise ; aucune valeur n'est recalculée |
Variant(variant) |
non | choisit le rendu Compact ou Detailed |
ShowVatBreakdown(value) |
non | affiche ou masque le détail des montants de TVA par taux |
ShowDiscounts(value) |
non | affiche ou masque les informations de remise |
CarryOverSubtotals(value) |
non | active ou désactive les lignes de report lors d'une coupure du tableau entre deux pages |
Chaque Section reçoit un LineSectionBuilder :
| Méthode | Obligatoire | Effet exact |
|---|---|---|
Line(description, total, quantity, unit, unitPrice, discountPercentage, detail) |
oui, au moins une fois | ajoute une ligne chiffrée avec un descriptif et un total obligatoires ; les autres valeurs alimentent leurs colonnes si elles sont visibles |
Subtotal(subtotal) |
non | affiche le sous-total pré-calculé à la fin de la section |
Paramètres de Line :
| Paramètre | Signification |
|---|---|
description |
libellé principal de la prestation ou du produit |
total |
montant final pré-calculé de cette ligne |
quantity |
quantité numérique |
unit |
unité textuelle, par exemple heure, jour ou forfait |
unitPrice |
prix unitaire pré-calculé |
discountPercentage |
pourcentage de remise déjà appliqué au total |
detail |
précision secondaire affichée avec la description |
.AddLineItems(items => items
.Currency("EUR")
.Columns(
LineItemColumn.Description,
LineItemColumn.Quantity,
LineItemColumn.Unit,
LineItemColumn.UnitPrice,
LineItemColumn.Discount,
LineItemColumn.Total)
.Variant(LineItemsVariant.Detailed)
.ShowDiscounts(true)
.ShowVatBreakdown(true)
.CarryOverSubtotals(true)
.Section("Étude", section => section
.Line(
description: "Analyse préliminaire",
detail: "Réunion et rapport de synthèse",
quantity: 2m,
unit: "jour",
unitPrice: 750m,
discountPercentage: 10m,
total: 1350m)
.Subtotal(1350m))
.Section("Exécution", section => section
.Line("Accompagnement", total: 2000m)
.Subtotal(2000m))
.Totals(
exclVat: 3350m,
inclVat: 4053.50m,
(Rate: 21m, Base: 3350m, Amount: 703.50m)))
Points importants :
Columnsdécide quelles colonnes apparaissent et dans quel ordre.- Tous les montants, sous-totaux, remises, bases TVA et totaux sont pré-calculés par l'application appelante. Le client et le service ne font aucune addition.
Currencyattend un code ISO 4217 tel queEUR.CarryOverSubtotals(true)active les lignes « à reporter » et « report » aux coupures de page. Cette option demande une passe de mesure supplémentaire.- Les variantes sont
CompactetDetailed.
Échéancier de paiement
Méthode de PaymentScheduleBuilder |
Obligatoire | Effet exact |
|---|---|---|
Heading(heading) |
non | ajoute un titre de section au-dessus de l'échéancier |
Currency(isoCode) |
oui | définit la devise de toutes les échéances |
Installment(label, amount, dueDate, note) |
oui, au moins une fois | ajoute une tranche avec son libellé et son montant ; la date d'échéance et la note sont facultatives |
.AddPaymentSchedule(schedule => schedule
.Heading("Échéancier")
.Currency("EUR")
.Installment("Acompte", 1210m, new DateOnly(2026, 9, 1))
.Installment("Solde", 2843.50m, new DateOnly(2026, 10, 1), "Après réception"))
Signature
Méthode de SignatureBuilder |
Obligatoire | Effet exact |
|---|---|---|
Place(place) |
non | affiche le lieu de signature |
Date(date) |
non | affiche la date de signature |
Signatory(name, role, caption, image) |
oui, au moins une fois | ajoute un signataire ; son nom est obligatoire, son rôle, sa légende et son image sont facultatifs |
Variant(variant) |
non | choisit une zone simple avec Single ou deux zones avec Dual |
byte[] signatureBytes = await File.ReadAllBytesAsync("signature.png");
// Dans la composition du document :
.AddSignature(signature => signature
.Place("Bruxelles")
.Date(new DateOnly(2026, 8, 20))
.Variant(SignatureVariant.Dual)
.Signatory(
"Alex Exemple",
role: "Administrateur",
caption: "Pour accord",
image: DataUri.Png(signatureBytes))
.Signatory("Sam Exemple", role: "Client"))
DataUri.Png, DataUri.Jpeg et DataUri.Svg encodent les images en ligne. N'envoyez pas
d'URL : le contexte de rendu n'a volontairement aucun accès réseau.
Conditions générales
Pour des conditions imprimées intégralement :
Méthode de LegalTermsBuilder |
Obligatoire | Effet exact |
|---|---|---|
Title(title) |
non | ajoute un titre général au-dessus de toutes les conditions |
Section(heading, paragraphs) |
oui, au moins une fois | ajoute une section ; son titre est facultatif mais elle doit contenir au moins un paragraphe |
Pour la variante en ligne, AddLegalTermsLink(url, linkText, title) affiche un titre général
facultatif, un texte d'introduction facultatif, puis l'URL obligatoire imprimée en entier et
cliquable.
Texte complet structuré :
.AddLegalTerms(terms => terms
.Title("Conditions générales")
.Section("1. Objet", "Premier paragraphe.", "Deuxième paragraphe.")
.Section("2. Paiement", "Les factures sont payables à l'échéance indiquée."))
Ou lien vers une version en ligne :
.AddLegalTermsLink(
"https://example.test/conditions-generales",
linkText: "Version complète :",
title: "Conditions générales")
La numérotation des sections appartient au contenu. Le lien est imprimé en entier et reste cliquable dans le PDF.
Espacement et saut de page
| Méthode | Effet exact |
|---|---|
AddSpacer(SpacerSize.Small) |
ajoute le plus petit espacement vertical prédéfini |
AddSpacer(SpacerSize.Medium) |
ajoute un espacement vertical intermédiaire prédéfini |
AddSpacer(SpacerSize.Large) |
ajoute le plus grand espacement vertical prédéfini |
AddPageBreak() |
termine la page courante ; le bloc ajouté ensuite commence sur une nouvelle page |
.AddSpacer(SpacerSize.Medium)
.AddPageBreak()
Privilégiez la pagination automatique. AddPageBreak sert uniquement lorsqu'un nouveau chapitre
doit réellement commencer sur une nouvelle page ; les règles de coupure internes aux blocs ne
sont pas configurables.
Libellés et locales
Les locales disponibles sont FrBE et NlBE. Elles déterminent les libellés par défaut ainsi
que le format des nombres et des dates.
OverrideLabels permet de remplacer uniquement les libellés mécaniques nécessaires au document :
.OverrideLabels(labels =>
{
labels.Parties = new PartiesLabels
{
Issuer = "Prestataire",
Recipient = "Client",
};
labels.TitleBlock = new TitleBlockLabels
{
Reference = "Numéro de facture",
Date = "Date d'émission",
};
labels.LineItems = new LineItemsLabels
{
Columns = new LineItemColumnLabels
{
Description = "Prestation",
},
TotalExclVat = "Total HTVA",
TotalInclVat = "Total TVAC",
VatAtRate = "TVA {rate} %",
};
labels.Footer = new FooterLabels
{
PageOfTotal = "Page {page} sur {total}",
};
labels.Watermark = new WatermarkLabels
{
Draft = "BROUILLON",
Duplicate = "DUPLICATA",
};
});
Les placeholders {rate}, {page} et {total} doivent être conservés dans leurs gabarits.
Une clé inconnue est rejetée par le service avec une erreur 422. Les textes métier, comme le
titre « Facture » ou « Offre », restent du contenu fourni dans les blocs.
Générer un PDF ou un aperçu
RenderedDocument document = await client.RenderAsync(request, cancellationToken);
Le résultat contient :
| Propriété | Description |
|---|---|
Pdf |
contenu binaire du PDF |
PageCount |
nombre de pages annoncé par le service |
ThemeHash |
empreinte du thème utilisé |
ContentHash |
empreinte de la requête complète, utilisable comme clé de cache |
ThemeWarnings |
avertissements de thème renvoyés pendant le rendu |
Le service ne conserve pas le PDF. L'application appelante est responsable de sa transmission, de son archivage et de son éventuel cache.
Exemple de réponse ASP.NET Core :
RenderedDocument document = await docGen.RenderAsync(request, cancellationToken);
return Results.File(document.Pdf, "application/pdf", "facture.pdf");
Aperçu PNG
byte[] png = await client.PreviewAsync(
request,
page: 1,
scale: 1.5,
cancellationToken);
page est indexé à partir de 1. L'aperçu découpe le flux continu en bandes : sa pagination peut
différer du PDF, notamment avec les lignes insécables et les bandeaux répétés. N'utilisez pas
RenderedDocument.PageCount comme borne des pages PNG ; le PDF reste le document de référence.
Erreurs et annulation
Validation locale
Le builder lève :
ArgumentExceptionpour une valeur obligatoire vide ou une collection vide ;ArgumentNullExceptionpour une dépendance ou une action de configuration nulle ;InvalidOperationExceptionlorsqu'un bloc incomplet est finalisé ou que le document n'a ni thème ni bloc.
Construisez la requête avant d'entrer dans la partie qui envoie une réponse HTTP afin de traiter séparément les erreurs de programmation et les erreurs du service.
Erreur du service
Toute réponse HTTP non réussie devient une DocGenApiException :
try
{
return await client.RenderAsync(request, cancellationToken);
}
catch (DocGenApiException error) when (error.StatusCode == 422)
{
foreach (DocGenFieldError field in error.Errors)
{
logger.LogWarning(
"Document invalide : {Path} [{Code}] {Message}",
field.Path,
field.Code,
field.Message);
}
throw;
}
L'exception expose StatusCode, ProblemType, Title, Detail et Errors. Chaque erreur de
champ contient Path, Code et Message. Les statuts courants sont 401 (clé invalide), 422
(contrat invalide), 429 (capacité saturée), 500 (échec interne) et 504 (délai de rendu dépassé).
Un CancellationToken est accepté par tous les appels asynchrones. Une annulation ou un délai
du HttpClient lève les exceptions HTTP/.NET habituelles, pas une DocGenApiException.
Ne journalisez jamais la requête ni les données du document : elles peuvent contenir des données personnelles. Journalisez un hash, une taille et les chemins d'erreur.
Tests
Le point d'injection est IDocGenClient. Une classe métier peut donc être testée avec le mécanisme
de doublure déjà utilisé par votre projet, sans démarrer Chromium ni produire un vrai PDF.
Les tests de cette bibliothèque se lancent depuis la racine du dépôt :
dotnet test clients/dotnet/JAK.DocGen.sln
Les tests d'intégration réels utilisent les variables suivantes :
export DOCGEN_BASE_URL=http://localhost:8090
export DOCGEN_API_KEY=dev-secret
dotnet test clients/dotnet/JAK.DocGen.sln
Sans ces deux variables, les scénarios qui nécessitent le service retournent immédiatement ; les tests unitaires du builder, de la sérialisation et de l'injection restent exécutés.
Règles d'intégration
- Résolvez et persistez le thème quand la charte change, pas à chaque rendu.
- Calculez les montants dans le domaine métier avant de construire
lineItems. - Utilisez uniquement des unités d'impression (
mm,pt) et des options enumérées. - Envoyez les images en Data URI ; aucune ressource distante n'est chargée au rendu.
- N'envoyez jamais de HTML, CSS, marge ou style libre.
- Archivez le PDF côté appelant si le document doit être conservé.
- Propagez un
CancellationTokenet configurez un délai HTTP supérieur au délai de rendu du service. - Traitez les 422 comme des erreurs de contrat et affichez les chemins fautifs pendant le développement.
La spécification complète du service et les règles de pagination se trouvent dans
docs/SPEC.md.
Taux de TVA par ligne (0.2.0)
Le service doit inclure le contrat 0.2.0 pour accepter vatRate. Le client n'effectue aucun
calcul de remise, de TVA ou de total : l'application transmet ses montants métier.
builder.AddLineItems(items => items
.Currency("EUR")
.Columns(LineItemColumn.Description, LineItemColumn.Discount, LineItemColumn.VatRate, LineItemColumn.Total)
.Section(null, section => section.Line("Prestation", total: 90m,
quantity: 1m, unitPrice: 100m, discountPercentage: 10m, vatRate: 21m))
.Totals(exclVat: 90m, inclVat: 108.9m, (21m, 90m, 18.9m)));
L'appelant omet la colonne Discount si aucune ligne n'a de remise. Le taux 0 est affiché ;
un taux absent reste absent. Les libellés LineItems.Columns.VatRate et LineItems.VatAtRate
sont surchargeables ; ce dernier accepte {rate} et {base} (montant avec devise).
Publication du client
Publication : pousser le tag dotnet-v0.3.0 sur le commit validé. La pipeline CI teste le
client contre le vrai moteur, vérifie la correspondance tag/version puis publie sur NuGet via
OIDC. Les artefacts du même run contiennent le contrat OpenAPI et les schémas de thème.
| 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 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. |
| .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
- Microsoft.Extensions.Http (>= 8.0.1)
- System.Text.Json (>= 8.0.5)
-
net8.0
- Microsoft.Extensions.Http (>= 8.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.