Skip to content

.NET client ​

BroadPaper.Client is a library for the render service, targeting both .NET 8 and .NET 10 so that the runtime is your choice rather than ours. Your users design reports in the browser; your ASP.NET Core application stores the template and renders it — on a schedule, in a background job, in a controller — without a browser, a JavaScript runtime or a native PDF library anywhere near the server.

bash
dotnet add package BroadPaper.Client
csharp
builder.Services.AddBroadPaper(o =>
{
    o.BaseUrl = builder.Configuration["BroadPaper:BaseUrl"]!;
    o.Token   = builder.Configuration["BroadPaper:Token"];
});

AddBroadPaper registers the client over a named HttpClient, so the pooling, logging, resilience policies and telemetry you have already configured apply to it too. Add your own handlers to the returned builder, or to the client named BroadPaperClient.HttpClientName.

Rendering ​

The template and the data are host-defined JSON documents, so the client takes them as JsonNode and does not try to model them in C#. That is deliberate: your schema is yours, and a strongly-typed mirror of it would be a second place to keep it up to date.

csharp
public sealed class ReviewController(BroadPaperClient broadpaper, IReviewStore store) : ControllerBase
{
    [HttpGet("clients/{id}/review.pdf")]
    public async Task<IActionResult> Review(string id, CancellationToken ct)
    {
        var review = await store.GetAsync(id, ct);

        var result = await broadpaper.RenderAsync(new RenderRequest
        {
            Template = JsonNode.Parse(review.TemplateJson),
            Data     = RenderRequest.ToNode(review.Data),
            Now      = review.AsAt,             // fixes the clock; see below
            Locale   = "en-GB",
            Currency = "GBP",
            Metadata = new RenderMetadata { Title = $"Investment review — {review.ClientName}", Lang = "en-GB" }
        }, ct);

        return File(result.Pdf, "application/pdf", $"review-{id}.pdf");
    }
}

RenderRequest.FromJson(templateJson, dataJson) is the shortcut when both are already strings out of a database column.

RenderResult carries the bytes, the page count, the backend that drew them and how long it took. Warnings is a count taken from the response header; when you want to know what they say — a font that could not be embedded, a style with no equivalent — RenderWithWarningsAsync asks for the JSON form and returns them in full. Warnings are worth logging and are not failures.

Batches ​

A monthly run is one call. The template is parsed once, the connection is used once, and each item succeeds or fails on its own.

csharp
var items = clients.Select(c => new BatchItem { Id = c.Id, Data = RenderRequest.ToNode(c) });
var results = await broadpaper.RenderBatchAsync(new RenderRequest { Template = template }, items, ct);

foreach (var r in results)
{
    if (r.Ok) await store.SaveAsync(r.Id, r.Pdf!, ct);
    else logger.LogWarning("Factsheet {Id} failed: {Error} ({Code})", r.Id, r.Error, r.ErrorCode);
}

One client with unusable data does not cost you the other four hundred and ninety-nine.

Failures ​

Everything the service refuses arrives as a BroadPaperException carrying a BroadPaperErrorCode — BadRequest, Unauthorised, PayloadTooLarge, BackendUnavailable, Timeout, TooManyPages, Busy, RenderFailed — plus Transport when the service could not be reached at all. A code this client does not recognise arrives as Unknown rather than throwing, so a newer service cannot break an older client.

IsTransient is true for exactly the three worth retrying:

csharp
catch (BroadPaperException e) when (e.IsTransient)
{
    // Busy, Timeout or Transport. The rest will fail again the same way.
}

Health and protocol ​

csharp
var health = await broadpaper.GetHealthAsync(ct);
if (health.Protocol != BroadPaperClient.SupportedProtocol)
    logger.LogWarning("Render service speaks protocol {Theirs}; this client speaks {Ours}",
                      health.Protocol, BroadPaperClient.SupportedProtocol);

Backends, Active and Queued are what a readiness probe or a dashboard wants.

Fonts ​

The service embeds whatever fonts it was configured with, and every TrueType or OpenType face in the theme's fonts — the same theme the designer used, so a font declared there needs nothing more. To send one per request that the theme does not declare — a client's own brand face, held in your database rather than on the render host — RenderFont.FromFile base64-encodes it, and RenderFont.Bytes takes an encoded one directly.

csharp
Fonts = [RenderFont.FromFile("Charter", "/fonts/Charter-Regular.ttf", weight: 400)]

The file is read on your machine and travels as bytes. The service never opens a path named by a request, in RenderFont.Src or in the theme: Src has to be a data: URI or an address on one of the service's allowed hosts. See the render service.

Reproducible bytes ​

Set Now. Given the same template, data, theme, fonts and clock, the service returns the same bytes every time — which is what makes a rendered report safe to hash, cache, or assert on in a test. Leave Now unset and a report with a "generated at" stamp differs on every call, correctly.

Now is a DateTimeOffset and is sent as UTC ISO-8601.

Testing ​

The client's own tests cover the wire format against a stub handler and need nothing running. The integration tests render real PDFs and are skipped unless a service is pointed at:

bash
docker run -d -p 4780:4780 -e BROADPAPER_TOKEN=dev broadpaper/server
cd dotnet
BROADPAPER_SERVICE_URL=http://127.0.0.1:4780 BROADPAPER_TOKEN=dev dotnet test

Without those variables the same command runs the unit tests and skips the rest, so a build agent with no container runtime still gets a useful answer.

Licensing ​

The client carries no licence and needs none. The render service it talks to renders under its own, set from BROADPAPER_LICENSE in the service's environment, and GET /health reports which: see the render service. While you evaluate, a free 60-day key from broadpaper.com/eval removes the mark from what the service produces. It needs a 1.0.0 or later service image. Production needs a paid licence. See Licensing.