You'll need an ASP.NET Core app on .NET 10, and Microsoft Edge, Google Chrome or Chromium on the machine that renders PDFs. Edge comes with Windows, so most Windows machines already have it. In Development, if no browser is installed, MarkupReporter downloads Chrome once by itself.
Install the package
In your web project's folder:
dotnet add package MarkupReporter
Register MarkupReporter
Add MarkupReporter to Program.cs. The Designer is only added in Development; in every other environment your app just renders PDFs, and the Designer's pages don't exist.
builder.Services.AddMarkupReporter(builder.Configuration.GetSection("MarkupReporter"));
if (builder.Environment.IsDevelopment())
builder.Services.AddMarkupReporterDesigner();
var app = builder.Build();
app.MapStaticAssets(); // or app.UseStaticFiles(): the Designer's files
if (app.Environment.IsDevelopment())
app.MapMarkupReporterDesigner(); // at /markupreporterThen tell it where to keep your report templates, in appsettings.json:
"MarkupReporter": {
"TemplatesFolder": "Reports"
}The folder is relative to your project. The Designer saves into it, and your app loads templates from it.
Set up your project file
Add this to your .csproj. It stops saving a report from restarting your app while you work (dotnet watch and Hot Reload would otherwise see the new files), and it publishes the compiled templates with your app:
<PropertyGroup> <DefaultItemExcludes>$(DefaultItemExcludes);Reports\**</DefaultItemExcludes> </PropertyGroup> <ItemGroup> <Content Include="Reports\*.html" CopyToPublishDirectory="PreserveNewest" Watch="false" /> </ItemGroup>
Only the .html files are needed at runtime. Keep the .mrx design files in source control so you can edit them later.
Design your first report
Run your app and open https://localhost:<port>/markupreporter. The Designer only answers on your own machine, in Development.
- Start from a template. Pick the invoice, sales report or account statement starter, or a blank page.
- Add your data. In the Data panel, click Add dataset and paste a JSON sample of the data your app will pass in. Name the dataset after the property you'll pass, for example
invoice. You can also connect a SQL Server query while designing (see the configuration reference). - Bind fields. Select a text box and click + next to a field to insert it. For a table, click + on a field of the list to add a column.
- Preview. Click Preview PDF to see the real PDF, rendered exactly as production will render it.
- Save. Click Save and name it
invoice. This writesReports/invoice.mrx(the design) andReports/invoice.html(the template your app renders).
If the design still has problems, Save keeps your work in the .mrx but leaves the previous .html alone, so your app keeps rendering the last version that worked. The problems list in the Designer shows what to fix.
Render the PDF in your code
Inject IReportBuilder and call BuildAsync with the template name and your data. The top-level property names must match the dataset names in the design. Matching ignores case, so your existing C# classes work as they are.
Minimal API
app.MapGet("/invoices/{id}.pdf", async (
int id, IReportBuilder reports, InvoiceService invoices) =>
{
var invoice = await invoices.GetAsync(id);
var report = await reports.BuildAsync("invoice", new { invoice });
return Results.File(report.Pdf, "application/pdf", report.FileName);
});Razor Pages
public class InvoiceModel(IReportBuilder reports, InvoiceService invoices)
: PageModel
{
public async Task<IActionResult> OnGetPdfAsync(int id, CancellationToken ct)
{
var invoice = await invoices.GetAsync(id);
var report = await reports.BuildAsync(
"invoice", new { invoice }, cancellationToken: ct);
return File(report.Pdf, "application/pdf", report.FileName);
}
}Link to it with asp-page="/Invoice" asp-page-handler="Pdf" asp-route-id="@invoice.Id".
MVC controller
public class InvoicesController(IReportBuilder reports, InvoiceService invoices)
: Controller
{
[HttpGet("invoices/{id}/pdf")]
public async Task<IActionResult> Pdf(int id, CancellationToken ct)
{
var invoice = await invoices.GetAsync(id);
var report = await reports.BuildAsync(
"invoice", new { invoice }, cancellationToken: ct);
return File(report.Pdf, "application/pdf", report.FileName);
}
}report.FileName comes from the design's output file name. To change the file name or the culture used for numbers, dates and currency for one render, pass new ReportRenderOptions { FileName = "INV-1042.pdf", Culture = "en-GB" } as the third argument.
A field prints blank? The most common cause is a name mismatch: if the dataset is called invoice, pass new { invoice }, not new { inv }. MarkupReporter logs a warning naming the missing property.
Add your licence key
Without a key, PDFs are watermarked in Development and refused in every other environment. Once you buy a licence, your key arrives by email. Keep it out of source control:
On your machine
dotnet user-secrets set "MarkupReporter:LicenseKey" "<your licence key>"
On a server
Set an environment variable called MarkupReporter__LicenseKey (two underscores), or use your secret store, such as Azure Key Vault. Restart the app after adding the key; the watermark goes away. Keys are checked offline, so nothing is ever sent to us.
Go to production
- A browser on the server. MarkupReporter finds Microsoft Edge, then Google Chrome, then Chromium in their standard locations. On Windows Server, Edge is usually already there. Outside Development it never downloads a browser, so make sure one is installed, or set
Browser:ExecutablePath. - Your templates. The
.htmlfiles in your templates folder are published with your app (step 3). - Your licence key, set as above.
- Linux containers: install the browser's runtime libraries, or start from an image that includes Chromium. If the app runs as root, set
Browser:NoSandboxtotrue; better still, run as a non-root user.
The Designer doesn't exist in production, and MarkupReporter never runs report SQL there: your app loads its own data and passes it in.
Configuration reference
All settings live in the MarkupReporter section of your configuration. Only TemplatesFolder and LicenseKey are usually needed.
| Setting | Default | What it does |
|---|---|---|
TemplatesFolder | none | Where templates are saved and loaded, relative to your project. |
LicenseKey | none | Your licence key. Keep it in user secrets or a secret store. |
Browser:Use | Auto | Auto (Edge, then Chrome, then Chromium), Edge or Chrome. |
Browser:ExecutablePath | auto | A specific browser executable. Overrides Use. |
Browser:MaxConcurrentRenders | 4 | How many PDFs render at once; further requests wait. |
Browser:RenderTimeout | 00:01:00 | The longest one PDF may take. |
Browser:NoSandbox | false | Only for browsers running as root, in some containers. |
Designer:Connections | none | SQL Server connections for the Designer: a display name mapped to a ConnectionStrings key. |
Designer:PreviewRows | 200 | Rows fetched from each SQL dataset when previewing. |
Designing against SQL Server
SQL is used only while designing, for the list of fields and preview rows. Use a read-only database login, and keep its connection string in user secrets:
"ConnectionStrings": {
"ReportingReadOnly": "Server=...;Database=...;User Id=report_reader;..."
},
"MarkupReporter": {
"Designer": {
"Connections": { "Reporting": "ReportingReadOnly" }
}
}Troubleshooting
| What you see | What to do |
|---|---|
/markupreporter shows a 404 | Check the app runs in Development, that both AddMarkupReporterDesigner() and MapMarkupReporterDesigner() are called, and that you opened it through localhost. |
| The Designer loads without styles | Add app.MapStaticAssets() or app.UseStaticFiles(). |
| The page reloads when you save | Add the project file settings from step 3. |
| A field prints blank | Match the top-level property name to the dataset name; check the log warning. |
| "No Microsoft Edge, Google Chrome or Chromium installation was found" | Install Edge or Chrome on the server, or set Browser:ExecutablePath. |
| "Microsoft Edge couldn't be started" | A company policy may block it from being controlled. Set Browser:Use to Chrome with Chrome installed. |
| PDFs are still watermarked | Check the key is set as MarkupReporter:LicenseKey (or MarkupReporter__LicenseKey) and restart the app. The Designer's Help → Copy diagnostic info shows the licence status. |
Still stuck? In the Designer, Help → Report a problem prepares an email with your versions filled in, or write to support@markupreporter.com.