Calco

C# / .NET

The C# connector targets .NET 8.0 with zero external dependencies. Every method is async and returns byte[] for rendered output.

Add the connector

There's no NuGet package to add (yet) — the client is a single, dependency-free file. Copy CalcoClient.cs into your project, keeping its namespace Calco;, and use it:

csharp
using Calco;

Quick start

csharp
using Calco;

using var client = new CalcoClient(
    "http://localhost:8080",
    apiKey: "dc_live_...",
    timeout: TimeSpan.FromMinutes(5));

CalcoClient is IDisposable — wrap it in using or register it as a singleton (see below). The key is sent as Authorization: Bearer <key> on every request.

Converting a file

csharp
byte[] pdf = await client.ConvertAsync(
    filePath: "report.xlsx",
    format: "pdf",
    dataJson: """{"company": "Acme"}""",
    sheet: "Summary",
    title: "Q4 Report");

await File.WriteAllBytesAsync("report.pdf", pdf);

You can convert from a stream instead of a path:

csharp
await using var stream = File.OpenRead("report.xlsx");
byte[] pdf = await client.ConvertAsync(stream, "report.xlsx", format: "pdf");

Extract the placeholder map with ScaffoldAsync:

csharp
var scaffold = await client.ScaffoldAsync("spreadsheet.xlsx");

Managing templates

csharp
// Create
var template = await client.CreateTemplateAsync(
    filePath: "template.xlsx", name: "Invoice", category: "finance", tags: "billing,monthly");

// List
var templates = await client.ListTemplatesAsync();
foreach (var t in templates.EnumerateArray())
    Console.WriteLine($"{t.GetProperty("id")}: {t.GetProperty("name")}");

// Update metadata
await client.UpdateTemplateAsync(id: "42", name: "Invoice v2", category: "accounting");

// Add a new version
await client.CreateTemplateVersionAsync(id: "42", filePath: "template_v2.xlsx");

// Delete
await client.DeleteTemplateAsync("42");

Rendering a stored template

csharp
byte[] html = await client.RenderTemplateVersionAsync(
    id: "42",
    version: "2",
    format: "html",
    dataJson: """{"customer": "Jane"}""",
    title: "Invoice #1001");

await File.WriteAllBytesAsync("invoice.html", html);

Batch rendering

Pass a JSON array of records and receive a ZIP archive:

csharp
var records = """[{"name":"Alice","amount":100},{"name":"Bob","amount":200}]""";

byte[] zip = await client.BatchRenderAsync(
    recordsJson: records,
    templateId: "42",
    version: "1",
    format: "pdf",
    titleTemplate: "invoice_{{name}}");

await File.WriteAllBytesAsync("invoices.zip", zip);

By-hash workflow

Every stored version exposes a content hash (sha256 of the file). Address a version directly by hash:

csharp
var meta = await client.GetTemplateByHashAsync("3c9f...d1ab");
byte[] pdf = await client.RenderByHashAsync("3c9f...d1ab", "pdf", dataJson: """{"customer":"Jane"}""");
byte[] xlsx = await client.DownloadByHashAsync("3c9f...d1ab", "xlsx");

Error handling

All errors throw CalcoException, which carries the HTTP status code:

csharp
try
{
    await client.ConvertAsync("missing.xlsx", "pdf");
}
catch (CalcoException ex)
{
    Console.WriteLine($"Status: {ex.StatusCode}");
    Console.WriteLine($"Message: {ex.Message}");
}

ASP.NET Core integration

Dependency injection

csharp
// Program.cs
builder.Services.AddSingleton<CalcoClient>(_ =>
    new CalcoClient(
        builder.Configuration["Calco:Url"]!,
        apiKey: builder.Configuration["Calco:ApiKey"]));

Minimal API

csharp
app.MapPost("/export", async (IFormFile file, string format, CalcoClient dc) =>
{
    await using var stream = file.OpenReadStream();
    byte[] result = await dc.ConvertAsync(stream, file.FileName, format);

    return Results.File(result,
        contentType: "application/octet-stream",
        fileDownloadName: $"export.{format}");
});

Controller

csharp
[ApiController]
[Route("api/[controller]")]
public class ReportsController : ControllerBase
{
    private readonly CalcoClient _dc;

    public ReportsController(CalcoClient dc) => _dc = dc;

    [HttpPost("convert")]
    public async Task<IActionResult> Convert(IFormFile file, [FromQuery] string format)
    {
        await using var stream = file.OpenReadStream();
        var bytes = await _dc.ConvertAsync(stream, file.FileName, format);
        return File(bytes, "application/octet-stream", $"converted.{format}");
    }
}

Configuration options

ParameterDefaultDescription
baseUrl—Root URL of the service (required)
apiKeynullBearer token for authentication
httpClientnullOptional pre-configured HttpClient
timeout120 secondsRequest timeout