Oicana

C# using ASP.NET

In this chapter, you'll integrate Oicana into a C# web service using ASP.NET Core. ASP.NET Core is Microsoft's modern, cross-platform framework for building web applications and APIs. We'll create a simple web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.


Note

This chapter assumes, that you have a working .NET 8 setup. If that is not the case, please follow the official Microsoft guide to install .NET on your machine.


Let's start with a fresh ASP.NET project by executing dotnet new webapidotnet new webapi in a new directory. The starter project has a single endpoint defined in Program.csProgram.cs. We can try that endpoint out in the swagger UI. Start up the service (dotnet rundotnet run) and follow the link printed in the terminal. If the page is empty, navigate to /swagger/swagger. In the swagger UI, expand the /weatherforecast/weatherforecast endpoint, press "Try it out", then "Execute". This will send an HTTP request to the running ASP.NET service and return made up weather data.

New service endpoint

We will define a new endpoint to compile our Oicana template to a PDF and return the PDF file to the user.


  1. Create a new directory in the .NET project called templatestemplates and copy example-0.1.0.zipexample-0.1.0.zip into that directory.
  2. Add the OicanaOicana NuGet package as a dependency with dotnet add package Oicana --prereleasedotnet add package Oicana --prerelease.
  3. Read the template file and prepare it for compilation at the beginning of Program.csProgram.cs:


    
                                          
    using Oicana.Config;
    
                                          
    using Oicana.Inputs;
    
                                          
    using Oicana.Template;
    
                                          

    
                                          
    var templateFile =
    
                                          
        await File.ReadAllBytesAsync("templates/example-0.1.0.zip");
    
                                          
    var template = new Template(templateFile);
    
                                          
    using Oicana.Config;
    
                                          
    using Oicana.Inputs;
    
                                          
    using Oicana.Template;
    
                                          

    
                                          
    var templateFile =
    
                                          
        await File.ReadAllBytesAsync("templates/example-0.1.0.zip");
    
                                          
    var template = new Template(templateFile);
    
                                          
    using Oicana.Config;
    
                                          
    using Oicana.Inputs;
    
                                          
    using Oicana.Template;
    
                                          

    
                                          
    var templateFile =
    
                                          
        await File.ReadAllBytesAsync("templates/example-0.1.0.zip");
    
                                          
    var template = new Template(templateFile);
    
                                          
    using Oicana.Config;
    
                                          
    using Oicana.Inputs;
    
                                          
    using Oicana.Template;
    
                                          

    
                                          
    var templateFile =
    
                                          
        await File.ReadAllBytesAsync("templates/example-0.1.0.zip");
    
                                          
    var template = new Template(templateFile);


    Note

    Template registration (the new Template()new Template() call) compiles the template once in development mode to warm up the Typst cache. This happens at startup and uses the developmentdevelopment value of the input you defined previously.


  4. Replace the generated /weatherforecast/weatherforecast endpoint with the following:

    
                                          
    app.MapPost("compile", () =>
    
                                          
    {
    
                                          
        var stream = template.Compile([], [], ExportOptions.Pdf(), new CompilationOptions(CompilationMode.Development));
    
                                          
        var now = DateTimeOffset.Now;
    
                                          
        return Results.File(
    
                                          
            fileStream: stream,
    
                                          
            contentType: "application/pdf",
    
                                          
            fileDownloadName: $"example_{now:yyyy_MM_dd_HH_mm_ss_ffff}.pdf"
    
                                          
        );
    
                                          
    })
    
                                          
    .WithOpenApi();
    
                                          
    app.MapPost("compile", () =>
    
                                          
    {
    
                                          
        var stream = template.Compile([], [], ExportOptions.Pdf(), new CompilationOptions(CompilationMode.Development));
    
                                          
        var now = DateTimeOffset.Now;
    
                                          
        return Results.File(
    
                                          
            fileStream: stream,
    
                                          
            contentType: "application/pdf",
    
                                          
            fileDownloadName: $"example_{now:yyyy_MM_dd_HH_mm_ss_ffff}.pdf"
    
                                          
        );
    
                                          
    })
    
                                          
    .WithOpenApi();
    
                                          
    app.MapPost("compile", () =>
    
                                          
    {
    
                                          
        var stream = template.Compile([], [], ExportOptions.Pdf(), new CompilationOptions(CompilationMode.Development));
    
                                          
        var now = DateTimeOffset.Now;
    
                                          
        return Results.File(
    
                                          
            fileStream: stream,
    
                                          
            contentType: "application/pdf",
    
                                          
            fileDownloadName: $"example_{now:yyyy_MM_dd_HH_mm_ss_ffff}.pdf"
    
                                          
        );
    
                                          
    })
    
                                          
    .WithOpenApi();
    
                                          
    app.MapPost("compile", () =>
    
                                          
    {
    
                                          
        var stream = template.Compile([], [], ExportOptions.Pdf(), new CompilationOptions(CompilationMode.Development));
    
                                          
        var now = DateTimeOffset.Now;
    
                                          
        return Results.File(
    
                                          
            fileStream: stream,
    
                                          
            contentType: "application/pdf",
    
                                          
            fileDownloadName: $"example_{now:yyyy_MM_dd_HH_mm_ss_ffff}.pdf"
    
                                          
        );
    
                                          
    })
    
                                          
    .WithOpenApi();

    This code defines a new POST endpoint at /compile/compile. For every request, it compiles the template to PDF with two empty input lists and returns the file. We use CompilationMode.DevelopmentCompilationMode.Development here to demonstrate how the template falls back to the development value you defined for the infoinfo input ("Chuck Norris"). In a later step we will explicitly set a value for the input.

After restarting the service and refreshing the swagger UI, you should see the new endpoint. Open up the endpoint description and click "Try it out" and "Execute" to send a request to the server. You should see a successful response with a download button for the PDF file.


The 200 response from calling an endpoint in Swagger UI. The response includes a link called "Download file".

About performance

The PDF generation should not take longer than a couple of milliseconds. You can look at the request duration in the network tab of your browser's debugging tools for an estimation. The first request to an APS.NET service can be significantly slower than later ones, because ASP.NET does some preparation during the first request.


For a better measurement of the compilation speed on your machine, you can use a StopwatchStopwatch in the endpoint code.

Passing inputs from C#

Now let's use the template with inputs that you defined in the previous chapter. First, make sure to update the packed template in your ASP.NET project. Run oicana packoicana pack in the template directory and replace example-0.1.0.zipexample-0.1.0.zip in the ASP.NET project with the new file.


Our compilecompile endpoint is currently calling template.Compile([], [], ExportOptions.Pdf(), new CompilationOptions(CompilationMode.Development))template.Compile([], [], ExportOptions.Pdf(), new CompilationOptions(CompilationMode.Development)). This compiles the template without any explicit inputs. The first empty array are the jsonjson inputs and the second one the blobblob inputs. Now we'll provide an input value and switch to production mode.


Change the endpoint to set the name input you defined earlier.


                                  
app.MapPost("compile", () =>

                                  
{

                                  
    var input = new TemplateJsonInput("info", JsonSerializer.Deserialize<JsonNode>("{ \"name\": \"Baby Yoda\" }")!);

                                  
    var stream = template.Compile([input], [], ExportOptions.Pdf(), new CompilationOptions(CompilationMode.Production));

                                  
    var now = DateTimeOffset.Now;

                                  
    // ... more code from before

                                  
});

                                  
app.MapPost("compile", () =>

                                  
{

                                  
    var input = new TemplateJsonInput("info", JsonSerializer.Deserialize<JsonNode>("{ \"name\": \"Baby Yoda\" }")!);

                                  
    var stream = template.Compile([input], [], ExportOptions.Pdf(), new CompilationOptions(CompilationMode.Production));

                                  
    var now = DateTimeOffset.Now;

                                  
    // ... more code from before

                                  
});

                                  
app.MapPost("compile", () =>

                                  
{

                                  
    var input = new TemplateJsonInput("info", JsonSerializer.Deserialize<JsonNode>("{ \"name\": \"Baby Yoda\" }")!);

                                  
    var stream = template.Compile([input], [], ExportOptions.Pdf(), new CompilationOptions(CompilationMode.Production));

                                  
    var now = DateTimeOffset.Now;

                                  
    // ... more code from before

                                  
});

                                  
app.MapPost("compile", () =>

                                  
{

                                  
    var input = new TemplateJsonInput("info", JsonSerializer.Deserialize<JsonNode>("{ \"name\": \"Baby Yoda\" }")!);

                                  
    var stream = template.Compile([input], [], ExportOptions.Pdf(), new CompilationOptions(CompilationMode.Production));

                                  
    var now = DateTimeOffset.Now;

                                  
    // ... more code from before

                                  
});


Notice that we switched to CompilationMode.ProductionCompilationMode.Production now that we're providing explicit input values. Production mode is the recommended default for all document compilation in your application - it ensures you never accidentally generate a document with test data. In production mode, the template will never fall back to development values. If an input value is missing in production mode and the input does not have a default value, the compilation will fail unless your template handles nonenone values for that input.


Calling the endpoint now, will result in a PDF with "Baby Yoda" instead of "Chuck Norris". Building on this minimal service, one could set input values based on database entries or the request payload. Take a look at the open source ASP.NET example project on GitHub for a more complete showcase of the Oicana C# integration.