Skip to content

.NET client

BroadPaper.Client is a .NET 8 library for the render service. 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.

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 BroadPaperErrorCodeBadRequest, 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. To send one per request — 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)]

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.