Tutorial 2: Building an Invoice Generator¶
Duration: 1 hour Difficulty: Intermediate Prerequisites: Tutorial 1: Hello World completed
What You'll Learn¶
- Working with collections (lists, arrays)
- Using loops to repeat content
- Loop special variables (
@index,@number,@first,@last,@count) - Creating dynamic tables with row loops
- Formatting numbers and dates in the template
- Building a real-world invoice template
The Goal¶
Build a complete invoice generator that produces professional invoices like this:
INVOICE #INV-2025-001 Date: 15.01.2025
Bill To:
Acme Corporation
123 Main Street
Springfield, IL 62701
Line Items:
┌────┬───────────────────────────────┬─────┬──────────┬────────────┐
│ # │ Description │ Qty │ Price │ Total │
├────┼───────────────────────────────┼─────┼──────────┼────────────┤
│ 1 │ Software Enterprise License │ 5 │ 499,00 € │ 2.495,00 € │
│ 2 │ Support & Maintenance (Annual)│ 5 │ 99,00 € │ 495,00 € │
│ 3 │ Training Package (2 days) │ 2 │ 250,00 € │ 500,00 € │
└────┴───────────────────────────────┴─────┴──────────┴────────────┘
Subtotal: 3.490,00 €
Tax: 663,10 €
Total: 4.153,10 €
Payment due within 30 days.
The numbers use German formatting because the processor is configured with the de-DE culture (Step 6).
Step 1: Understanding Loops¶
Before building the invoice, let's understand how loops work.
Basic Loop Syntax¶
Template (each marker in its own paragraph):
Data:
Output:
For a collection of simple values such as strings, {{.}} (or {{this}}) is the current item.
The {{#foreach}} and {{/foreach}} markers must each be in a paragraph of their own: the paragraphs containing
the markers are removed from the output, including any other text in them.
Loop with Objects¶
Template:
Data:
["Products"] = new List<object>
{
new { Name = "Laptop", Price = 999.00m },
new { Name = "Mouse", Price = 29.00m },
new { Name = "Keyboard", Price = 79.00m }
}
Output (with an English culture; see Format Specifiers):
Inside the loop, {{Name}} and {{Price}} are properties of the current item. Names that the item does not have
are looked up in the outer data.
Step 2: Loop Special Variables¶
Inside loops, you have access to special variables:
| Variable | Description | Example |
|---|---|---|
@index |
Current position (0-based) | 0, 1, 2, ... |
@number |
Current position (1-based) | 1, 2, 3, ... |
@first |
True for first item | True, False, False |
@last |
True for last item | False, False, True |
@count |
Total number of items | 3, 3, 3 |
Template Example:
{{#foreach Items}}
Item {{@number}} of {{@count}}: {{.}}{{#if @last}} (final item){{/if}}
{{/foreach}}
Output:
These variables only exist inside a loop. Outside a loop, {{@count}} is a missing variable; to print the
number of items there, use the collection's Count property: {{Items.Count}}.
Step 3: Create the Invoice Template¶
Open Word and create invoice-template.docx:
INVOICE #{{InvoiceNumber}} Date: {{InvoiceDate:date:dd.MM.yyyy}}
Bill To:
{{BillTo.CompanyName}}
{{BillTo.Street}}
{{BillTo.City}}, {{BillTo.State}} {{BillTo.Zip}}
Line Items:
Now create a table with this structure. The loop markers go into their own rows; Templify repeats the rows between them for each item and removes the marker rows:
| # | Description | Quantity | Unit Price | Total |
|---|---|---|---|---|
| {{#foreach LineItems}} | ||||
| {{@number}} | {{Description}} | {{Quantity}} | {{UnitPrice:currency}} | {{LineTotal:currency}} |
| {{/foreach}} |
After the table, add:
:currency and :date:... format numbers and dates with the processor's culture, so the data can stay numeric.
Step 4: Define Data Classes¶
public class Invoice
{
public string InvoiceNumber { get; set; } = "";
public DateTime InvoiceDate { get; set; }
public Address BillTo { get; set; } = new();
public List<LineItem> LineItems { get; set; } = new();
public decimal Subtotal { get; set; }
public decimal Tax { get; set; }
public decimal Total { get; set; }
public string PaymentTerms { get; set; } = "";
}
public class Address
{
public string CompanyName { get; set; } = "";
public string Street { get; set; } = "";
public string City { get; set; } = "";
public string State { get; set; } = "";
public string Zip { get; set; } = "";
}
public class LineItem
{
public string Description { get; set; } = "";
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal LineTotal => Quantity * UnitPrice;
}
Step 5: Generate Invoice Data¶
Calculations belong in code; the template only displays the results:
public static class InvoiceFactory
{
private const decimal TaxRate = 0.19m; // 19% VAT
public static Invoice CreateSampleInvoice()
{
var lineItems = new List<LineItem>
{
new LineItem { Description = "Software Enterprise License", Quantity = 5, UnitPrice = 499.00m },
new LineItem { Description = "Support & Maintenance (Annual)", Quantity = 5, UnitPrice = 99.00m },
new LineItem { Description = "Training Package (2 days)", Quantity = 2, UnitPrice = 250.00m }
};
decimal subtotal = lineItems.Sum(item => item.LineTotal);
decimal tax = Math.Round(subtotal * TaxRate, 2);
return new Invoice
{
InvoiceNumber = "INV-2025-001",
InvoiceDate = new DateTime(2025, 1, 15),
BillTo = new Address
{
CompanyName = "Acme Corporation",
Street = "123 Main Street",
City = "Springfield",
State = "IL",
Zip = "62701"
},
LineItems = lineItems,
Subtotal = subtotal,
Tax = tax,
Total = subtotal + tax,
PaymentTerms = "Payment due within 30 days. Thank you for your business!"
};
}
}
Step 6: Process the Invoice¶
using System.Globalization;
using TriasDev.Templify.Core;
Invoice invoice = InvoiceFactory.CreateSampleInvoice();
var data = new Dictionary<string, object>
{
["InvoiceNumber"] = invoice.InvoiceNumber,
["InvoiceDate"] = invoice.InvoiceDate,
["BillTo"] = invoice.BillTo,
["LineItems"] = invoice.LineItems,
["Subtotal"] = invoice.Subtotal,
["Tax"] = invoice.Tax,
["Total"] = invoice.Total,
["PaymentTerms"] = invoice.PaymentTerms
};
var processor = new DocumentTemplateProcessor(new PlaceholderReplacementOptions
{
Culture = CultureInfo.GetCultureInfo("de-DE")
});
using var templateStream = File.OpenRead("invoice-template.docx");
using var outputStream = File.Create($"invoice-{invoice.InvoiceNumber}.docx");
ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data);
if (result.IsSuccess)
{
Console.WriteLine($"✓ Invoice {invoice.InvoiceNumber} generated!");
Console.WriteLine($" Line items: {invoice.LineItems.Count}");
Console.WriteLine($" Total: {invoice.Total:C}");
}
else
{
Console.WriteLine($"✗ Error: {result.ErrorMessage}");
}
Place the classes from Step 4 and Step 5 after these statements in Program.cs (or in separate files).
Step 7: Formatting Choices¶
The template decides how values look; the data stays typed:
| Template | Value | Output (de-DE) |
Output (en-US) |
|---|---|---|---|
{{Total:currency}} |
4153.10m |
4.153,10 € |
$4,153.10 |
{{Total:number:N2}} |
4153.10m |
4.153,10 |
4,153.10 |
{{InvoiceDate:date:dd.MM.yyyy}} |
2025-01-15 |
15.01.2025 |
15.01.2025 |
{{InvoiceDate:date:MMMM d, yyyy}} |
2025-01-15 |
Januar 15, 2025 |
January 15, 2025 |
Format specifiers only apply to values of the matching type: a value that is already a string (for example
"€499.00") is inserted as it is. If you need a format that the specifiers cannot express, format the value in
code and pass the string. See Format Specifiers for the full list.
Step 8: Batch Invoice Generation¶
Generate multiple invoices at once. The processor can be reused (and shared between threads):
public static void GenerateInvoices(DocumentTemplateProcessor processor, IEnumerable<Invoice> invoices)
{
Directory.CreateDirectory("invoices");
byte[] template = File.ReadAllBytes("invoice-template.docx");
foreach (Invoice invoice in invoices)
{
var data = new Dictionary<string, object>
{
["InvoiceNumber"] = invoice.InvoiceNumber,
["InvoiceDate"] = invoice.InvoiceDate,
["BillTo"] = invoice.BillTo,
["LineItems"] = invoice.LineItems,
["Subtotal"] = invoice.Subtotal,
["Tax"] = invoice.Tax,
["Total"] = invoice.Total,
["PaymentTerms"] = invoice.PaymentTerms
};
// Bytes in, bytes out: the template is read once
ProcessingResult result = processor.ProcessTemplate(template, data, out byte[] output);
if (result.IsSuccess)
{
File.WriteAllBytes($"invoices/invoice-{invoice.InvoiceNumber}.docx", output);
Console.WriteLine($"✓ Generated: {invoice.InvoiceNumber}");
}
else
{
Console.WriteLine($"✗ Failed: {invoice.InvoiceNumber} - {result.ErrorMessage}");
}
}
}
Complete Production-Ready Example¶
using System.Globalization;
using TriasDev.Templify.Core;
public class InvoiceGenerator
{
private readonly DocumentTemplateProcessor _processor;
private readonly string _templatePath;
public InvoiceGenerator(string templatePath)
{
_processor = new DocumentTemplateProcessor(new PlaceholderReplacementOptions
{
Culture = CultureInfo.GetCultureInfo("de-DE")
});
_templatePath = templatePath;
}
public bool GenerateInvoice(Invoice invoice, string outputPath)
{
var data = new Dictionary<string, object>
{
["InvoiceNumber"] = invoice.InvoiceNumber,
["InvoiceDate"] = invoice.InvoiceDate,
["BillTo"] = invoice.BillTo,
["LineItems"] = invoice.LineItems,
["Subtotal"] = invoice.Subtotal,
["Tax"] = invoice.Tax,
["Total"] = invoice.Total,
["PaymentTerms"] = invoice.PaymentTerms
};
try
{
// Writes the output file only when processing succeeds
ProcessingResult result = _processor.ProcessTemplateFile(_templatePath, outputPath, data);
if (!result.IsSuccess)
{
Console.WriteLine($"Invoice generation failed: {result.ErrorMessage}");
return false;
}
if (result.MissingVariables.Count > 0)
{
Console.WriteLine($"Warning - missing variables: {string.Join(", ", result.MissingVariables)}");
}
return true;
}
catch (IOException ex)
{
Console.WriteLine($"Error reading or writing files: {ex.Message}");
return false;
}
}
}
// Usage
var generator = new InvoiceGenerator("templates/invoice-template.docx");
Invoice invoice = InvoiceFactory.CreateSampleInvoice();
Directory.CreateDirectory("output");
if (generator.GenerateInvoice(invoice, $"output/invoice-{invoice.InvoiceNumber}.docx"))
{
Console.WriteLine("✓ Invoice generated successfully!");
}
What You Learned¶
✅ Collections - Working with lists and arrays
✅ Loops - Repeating content with {{#foreach}}
✅ Loop variables - Using @index, @number, @first, @last, @count
✅ Table row loops - Dynamic table generation with marker rows
✅ Calculations - Computing values in code
✅ Formatting - Currency, number and date format specifiers
✅ Real-world patterns - Production-ready invoice generation
✅ Batch processing - Generating multiple documents
Next Steps¶
- Conditionals - Show or hide content based on data
- Loops - Nested loops, named iteration variables, table loops in detail
- Boolean Expressions - Logic inside placeholders
Additional Resources¶
- Examples Collection - loops and tables
- FAQ - Loops
- FAQ - Loops in Tables
Happy invoicing! 💰