If your ASP.NET Core app needs to produce invoices, contracts or transactional emails, you usually end up choosing between a PDF library you template by hand and a separate SaaS you call over HTTP. TemplateMaster offers a third option: a NuGet package, TemplateMaster.AspNetCore, that embeds a visual template editor, its API and a C# service inside your own application.
Your users stay your users, your data stays in your database, and generating a PDF becomes a method call instead of an HTTP request.
🧭 What you get
| Package | TemplateMaster.AspNetCore |
| Platform | .NET 8 · ASP.NET Core |
| Editor | Visual template editor served under /templatemaster (UI bundled, no Node.js needed) |
| C# API |
ITemplateMaster — generate PDFs and send emails straight from your code |
| Auth | Reuses your app's authentication — there's no separate TemplateMaster login |
📦 Prerequisites
- .NET 8 and an ASP.NET Core application
- SQL Server 2019+: a dedicated database for TemplateMaster (created automatically in development)
-
RabbitMQ in production (in development,
UseInMemoryMessaging()is enough) - Cookie-based authentication in your app (Identity, OpenID Connect, cookie…). TemplateMaster maps your signed-in users to its own users; it doesn't ship its own sign-in page.
1️⃣ Install the package
dotnet add package TemplateMaster.AspNetCore
That's the only package to reference. The whole TemplateMaster layer is included; only third-party dependencies (EF Core, MassTransit, PuppeteerSharp…) are pulled from nuget.org.
2️⃣ Configure appsettings.json
TemplateMaster reads its settings from a dedicated TemplateMaster section and never touches your other keys.
{
"ConnectionStrings": {
"TemplateMaster": "Server=localhost;Database=TemplateMaster;Trusted_Connection=True;TrustServerCertificate=True"
},
"RabbitMq": {
"Host": "localhost",
"Username": "guest",
"Password": "guest"
},
"TemplateMaster": {
"LicenseKey": "<your license key - keep it in user secrets / environment variables>",
"PathRoot": "D:/templatemaster/files",
"EmailSettings": {
"SMTPSetting": { "Host": "smtp.example.com" },
"Port": 587,
"UserName": "noreply@example.com"
}
}
}
PathRoot is where generated documents are stored on disk (it's printed at startup).
3️⃣ Wire it up in Program.cs
using TemplateMaster;
var builder = WebApplication.CreateBuilder(args);
// Your application's own authentication (cookie, Identity, OpenID Connect...)
builder.Services.AddAuthentication(/* ... */);
builder.Services.AddAuthorization(options =>
options.AddPolicy("TemplateMasterAdmin", policy => policy.RequireRole("Admin")));
builder.Services
.AddTemplateMaster(options =>
{
options.BasePath = "/templatemaster";
options.Database.AutoCreate = builder.Environment.IsDevelopment();
})
.UseSqlServer(builder.Configuration.GetConnectionString("TemplateMaster")!)
.UseRabbitMq(options =>
{
options.Host = builder.Configuration["RabbitMq:Host"]!;
options.Username = builder.Configuration["RabbitMq:Username"]!;
options.Password = builder.Configuration["RabbitMq:Password"]!;
})
.AddLicenseKey(licenseKey: builder.Configuration["TemplateMaster:LicenseKey"]);
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// TemplateMaster editor + API, reserved to your administrators
app.MapTemplateMaster("/templatemaster")
.RequireAuthorization("TemplateMasterAdmin");
app.Run();
Three things worth understanding here:
-
Database.AutoCreatecreates the database on first run. Keep it for development only; in production, deploy the schema with the SQL scripts. -
The dashboard always requires a signed-in user.
RequireAuthorization(...)lets you add your own rules on top, like theAdminrole policy above. -
Messaging:
UseRabbitMqis for production. For local development,UseInMemoryMessaging()saves you from running a broker.
4️⃣ Activate your license
Without a license, generated PDFs and emails carry a watermark and the free-plan limits apply: 10 documents and 10 emails per month. That's plenty to try everything out.
The key goes through AddLicenseKey, matching the TemplateMaster:LicenseKey entry of your configuration. Since it's a secret, keep it out of source control:
# Development: user secrets
dotnet user-secrets set "TemplateMaster:LicenseKey" "<your-key>"
# Production: environment variable
TemplateMaster__LicenseKey=<your-key>
At startup, the result is written to your logs:
info: TemplateMaster.License[0]
TemplateMaster license: VALID - client acme, issued 2026-10-03, expires 2027-10-03.
If the key is rejected, the log explains why, but an invalid key never prevents your app from starting.
5️⃣ Open the editor
Run the app, sign in with your application's login, and go to /templatemaster. A default invoice template and a default email template are already created for you.
👥 Users and organizations
There's no TemplateMaster login screen. A user signed in to your app is automatically matched (by email) to a TemplateMaster user and organization, created on their first visit. Two models:
- One organization (default): all your users share the same templates.
-
One organization per customer (
Tenancy.Mode = HostManaged+TenantKeySelector): each customer of your app gets isolated templates and documents, exactly like on the SaaS platform. Perfect for B2B multi-tenant apps.
6️⃣ Generate PDFs and emails from C
Inject ITemplateMaster into your services. Every call applies the same rules as the REST API: validation, per-organization isolation, plan limits and document history. No HTTP call involved.
| Method | Purpose |
|---|---|
GetTemplatesAsync(type?) |
List the organization's templates (code, name, type) |
RenderHtmlAsync(code, model) |
Render a template to HTML with your data, without saving anything |
GeneratePdfAsync(code, model, fileName?) |
Generate a PDF immediately and add it to the document history |
QueueDocumentAsync(code, model) |
Queue a PDF generation, processed in the background |
SendEmailAsync(email) |
Send an email from an email template |
ForUser(user) |
Same API, on behalf of the signed-in user (their organization, their role) |
ForTenant(tenantKey) |
Same API, for a given customer (one-organization-per-customer mode) |
Here's an invoice service:
using TemplateMaster;
public sealed class InvoiceService(ITemplateMaster templateMaster)
{
public async Task<byte[]> CreatePdfAsync(Invoice invoice, CancellationToken ct)
{
var template = (await templateMaster.GetTemplatesAsync(TemplateMasterTemplateType.Document, ct))
.Single(t => t.Name == "invoice");
return await templateMaster.GeneratePdfAsync(
template.Code,
new
{
invoice_number = invoice.Number,
customer_name = invoice.CustomerName,
items = invoice.Lines.Select(l => new { description = l.Label, quantity = l.Quantity })
},
fileName: $"{invoice.Number}.pdf",
cancellationToken: ct);
}
}
And expose it with a minimal API endpoint:
app.MapGet("/invoices/{id:int}/pdf", async (int id, InvoiceService invoices, CancellationToken ct) =>
{
var pdf = await invoices.CreatePdfAsync(await LoadInvoiceAsync(id, ct), ct);
return Results.File(pdf, "application/pdf", $"invoice-{id}.pdf");
}).RequireAuthorization();
⚠️ Model property names must match the template variables.
{{invoice_number}}expects a property calledinvoice_number. Use an anonymous object, a dictionary, or[JsonPropertyName]to control the names. This is the #1 cause of "empty" fields in generated documents.
When a PDF takes time or you generate many at once, prefer QueueDocumentAsync: the work happens in the background through the message bus instead of blocking your request.
🚦 Before going to production
A checklist straight from the docs:
- [ ] Database schema deployed with SQL scripts,
Database.AutoCreatedisabled - [ ]
UseRabbitMqwith a dedicated virtual host if the broker is shared - [ ] License and secrets (SMTP,
Authentication:ClientSecret) in environment variables - [ ]
PathRooton a persistent volume (shared between instances); Redis configured if you run several instances - [ ] On Linux / Docker: Chromium installed (
/usr/bin/chromium), required for PDF generation
That last point trips people up in containers: PDF rendering is browser-based (PuppeteerSharp), so a slim base image without Chromium will fail at generation time. A minimal Dockerfile addition on Debian-based images:
RUN apt-get update \
&& apt-get install -y --no-install-recommends chromium \
&& rm -rf /var/lib/apt/lists/*
🎯 Wrapping up
In a handful of lines you get a visual editor for your non-developer colleagues, a typed C# API for generating PDFs and emails, per-customer isolation if you need it, and no extra login to manage. If you want to see everything working end to end (login, PDF generation, background jobs, emails), the TemplateMaster team published a complete TemplateMasterDemo app on GitHub, linked from the docs.
Want to try it without any .NET setup first? Check out my previous post on running the whole TemplateMaster stack locally with Docker Compose.
👉 Full documentation: templatemaster.fr/fr/documentation/dotnet-integration
Are you generating PDFs in your .NET apps today? Which library are you using? Let me know in the comments! 👇
Top comments (0)