MarkupReporter

Get started

From an empty ASP.NET Core project to your first PDF in about five minutes. You don't need a licence key to follow along: in Development everything works, and PDFs carry a watermark until you add one.

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:

Terminal
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.

Program.cs
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 /markupreporter

Then tell it where to keep your report templates, in appsettings.json:

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:

YourApp.csproj
<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.

  1. Start from a template. Pick the invoice, sales report or account statement starter, or a blank page.
  2. 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).
  3. 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.
  4. Preview. Click Preview PDF to see the real PDF, rendered exactly as production will render it.
  5. Save. Click Save and name it invoice. This writes Reports/invoice.mrx (the design) and Reports/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

Program.cs
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

Pages/Invoice.cshtml.cs
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

Controllers/InvoicesController.cs
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

Terminal
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 .html files 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:NoSandbox to true; 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.

SettingDefaultWhat it does
TemplatesFoldernoneWhere templates are saved and loaded, relative to your project.
LicenseKeynoneYour licence key. Keep it in user secrets or a secret store.
Browser:UseAutoAuto (Edge, then Chrome, then Chromium), Edge or Chrome.
Browser:ExecutablePathautoA specific browser executable. Overrides Use.
Browser:MaxConcurrentRenders4How many PDFs render at once; further requests wait.
Browser:RenderTimeout00:01:00The longest one PDF may take.
Browser:NoSandboxfalseOnly for browsers running as root, in some containers.
Designer:ConnectionsnoneSQL Server connections for the Designer: a display name mapped to a ConnectionStrings key.
Designer:PreviewRows200Rows 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:

appsettings.Development.json
"ConnectionStrings": {
  "ReportingReadOnly": "Server=...;Database=...;User Id=report_reader;..."
},
"MarkupReporter": {
  "Designer": {
    "Connections": { "Reporting": "ReportingReadOnly" }
  }
}

Troubleshooting

What you seeWhat to do
/markupreporter shows a 404Check the app runs in Development, that both AddMarkupReporterDesigner() and MapMarkupReporterDesigner() are called, and that you opened it through localhost.
The Designer loads without stylesAdd app.MapStaticAssets() or app.UseStaticFiles().
The page reloads when you saveAdd the project file settings from step 3.
A field prints blankMatch 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 watermarkedCheck 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.