Reporting — Create Reports in Node.js Apps via .NET And WebAssembly Integration

Reporting Team Blog
17 April 2024

Why Node.js and WebAssembly?

Customizing DevExpress Web Reports (within a back-end application) may present unique challenges to front end-web developers new to .NET. In this post, we'll show you how the integration of .NET and WebAssembly can help address these challenges and enhance overall app security/performance.

The conventional approach to report development/customization requires expertise in .NET and related languages...but WebAssembly eliminates this requirement. By running .NET applications in a WASM environment, developers can integrate interactive reports into Node.js applications without in-depth knowledge of .NET.

A second benefit of this integration is security-related. WebAssembly executes code in an isolated environment. As such, developers have full control over disc, network, and other critical resources. This isolated execution environment can reduce risks associated with unauthorized access.

Microsoft has been working on the integration of .NET and WebAssembly in recent .NET updates. In .NET 7, Micrsooft introduced CLI templates (such as wasmconsole and wasmbrowser ) and allowed developers to create console and web applications that run in a sandboxed WebAssembly environment which hosts the .NET runtime. See the following blog post and YouTube video to learn more in this regard:

With the release of .NET 8, application code translates directly into WebAssembly at compile time. This change yields dramatic performance-related improvements, characterized by reduced latency and a more responsive user interface.

Try It Out Yourself

If you are a front-end web developer new to .NET and you are interested in the topic of this blog post, we suggest that you create an application that will allow you to create a DevExpress report and export it to a PDF file.

Before You Start

  • Install .NET 8 SDK.
  • Install the most recent CLI templates:
    
    >dotnet new install Microsoft.NET.Runtime.WebAssembly.Templates::8.0.3 
    
  • Install the wasm-tools workload:
    
    >dotnet workload install wasm-tools
    

Create A Simple Wasmconsole App

Run the following command to create a sample wasm-exporter app:


>dotnet new wasmconsole -o wasm-exporter 

When the sample project is ready, navigate to the project folder:


>cd wasm-exporter

The Program.cs file contains the following code:


using System;
using System.Runtime.InteropServices.JavaScript;

Console.WriteLine("Hello, Console!");

return 0;

public partial class MyClass
{
    [JSExport]
    internal static string Greeting()
    {
        var text = $"Hello, World! Greetings from node version: {GetNodeVersion()}";
        return text;
    }

    [JSImport("node.process.version", "main.mjs")]
    internal static partial string GetNodeVersion();
}

As you can see, JavaScript imports the Greeting .NET function, whereas .NET itself imports a function that displays the currently installed version of Node.js.

In turn, the code in the main.mjs file loads the .NET runtime and imports a JavaScript function into WebAssembly:


import { dotnet } from './_framework/dotnet.js'

const { setModuleImports, getAssemblyExports, getConfig } = await dotnet
    .withDiagnosticTracing(false)
    .create();

setModuleImports('main.mjs', {
    node: {
        process: {
            version: () => globalThis.process.version
        }
    }
});

const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);
const text = exports.MyClass.Greeting();
console.log(text);

await dotnet.run();

Once you build this app using the dotnet build command, run it the same manner you usually run node.js apps to view the execution result of both functions:



>dotnet build
>node main.mjs
Hello, World! Greetings from node version: v18.12.1
Hello, Console!

Specify Configuration Settings to Create DevExpress Reports

If you are running macOS/Linux or Windows without the DevExpress installation, do the following:

  • create a nuget.config file:
dotnet new nugetconfig
  • remove the line <clear /> in the nuget.config file
  • update the line <add key="nuget" value="https://api.nuget.org/v3/index.json" /> and replace the nuget key with a custom feed name and replace the value with your DevExpress NuGet Feed URL obtained from the DevExpress NuGet Gallery page.

We recently added support for the NuGet 3 protocol. Learn more in the following blog post: NuGet v3 Support and Enhanced Localization across WinForms, WPF, ASP.NET Platforms — Early Access Preview (v23.2)

Once complete, install NuGet packages required for document creation within WebAssembly apps:


dotnet add package Newtonsoft.Json
dotnet add package DevExpress.Drawing.Skia --version 23.2.*-*
dotnet add package DevExpress.Reporting.Core --version 23.2.*-*
dotnet add package SkiaSharp.HarfBuzz --version 2.88.7
dotnet add package SkiaSharp.NativeAssets.WebAssembly --version 2.88.7
dotnet add package HarfBuzzSharp.NativeAssets.WebAssembly --version 2.8.2.4
dotnet add package SkiaSharp.Views.Blazor --version 2.88.7

Add a native SkiaSharp dependency to the project configuration file (wasm-exporter.csproj):


<ItemGroup>
    <NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\*.a" />
</ItemGroup>

Specify a path to the resulting executable and libraries:


<wasmappdir>./result</wasmappdir>

At this point, we are done with the prep and are ready to start coding.

Our application consists of two sections: a server .NET-based application compiles into an assembly, and a JavaScript client-side application creates and configures the .NET runtime environment to run that assembly in WASM.

.NET Solution

Open the folder in your favorite code editor to implement the following classes in the Program.cs code unit: ReportExporter, JsonSourceCustomizationService, and SimplifiedUriJsonSource:


using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices.JavaScript;
using System.Threading;
using System.Threading.Tasks;
using DevExpress.Data;
using DevExpress.DataAccess.Json;
using DevExpress.XtraPrinting;
using DevExpress.XtraReports.UI;

return 0;

public partial class ReportExporter {
    [JSExport]
    internal static async Task ExportToPdfAsync(JSObject exportModel, JSObject result) {
        using var report = new XtraReport();
        ((IServiceContainer)report).AddService(typeof(IJsonSourceCustomizationService), new JsonSourceCustomizationService());

        using var reportStream = new MemoryStream(exportModel.GetPropertyAsByteArray("reportXml"));
        report.LoadLayoutFromXml(reportStream, true);

        PdfExportOptions pdfOptions = report.ExportOptions.Pdf;
        if(exportModel.HasProperty("exportOptions")) {
            SimplifiedFillExportOptions(pdfOptions, exportModel.GetPropertyAsJSObject("exportOptions"));
        }

        using var resultStream = new MemoryStream();
        await report.ExportToPdfAsync(resultStream, pdfOptions);
        result.SetProperty("pdf", resultStream.ToArray());
        resultStream.Close();
    }

    static void SimplifiedFillExportOptions(object exportOptions, JSObject jsExportOptions) {
        PropertyInfo[] propInfos = exportOptions.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
        foreach(PropertyInfo pi in propInfos) {
            if(!jsExportOptions.HasProperty(pi.Name))
                continue;

            if(pi.PropertyType == typeof(bool)) {
                pi.SetValue(exportOptions, jsExportOptions.GetPropertyAsBoolean(pi.Name));

            } else if(pi.PropertyType == typeof(string)) {
                pi.SetValue(exportOptions, jsExportOptions.GetPropertyAsString(pi.Name));

            } else if(pi.PropertyType.IsEnum) {
                string val = jsExportOptions.GetPropertyAsString(pi.Name);
                if(Enum.IsDefined(pi.PropertyType, val)) {
                    pi.SetValue(exportOptions, Enum.Parse(pi.PropertyType, val));
                }

            } else if(pi.PropertyType.IsClass) {
                SimplifiedFillExportOptions(pi.GetValue(exportOptions), jsExportOptions.GetPropertyAsJSObject(pi.Name));
            }
        }
    }
}
public class JsonSourceCustomizationService : IJsonSourceCustomizationService {
    public JsonSourceBase CustomizeJsonSource(JsonDataSource jsonDataSource) {
        return jsonDataSource.JsonSource is UriJsonSource uriJsonSource ? new SimplifiedUriJsonSource(uriJsonSource.Uri) : jsonDataSource.JsonSource;
    }
}
public partial class SimplifiedUriJsonSource : UriJsonSource {
    public SimplifiedUriJsonSource(Uri uri) : base(uri) { }
    public override Task GetJsonStringAsync(IEnumerable sourceParameters, CancellationToken cancellationToken) {
        return GetJsonData(Uri.ToString());
    }
    [JSImport("getJsonData", "main.mjs")]
    internal static partial Task GetJsonData(string url);
}

ReportExporter

This class implements and exports the ExportToPdfAsync method to a JavaScript module. This method creates an XtraReport instance, adds the JsonSourceCustomizationService custom service to the report object model, and maps optional export options from the JavaScipt object to a native .NET object. A report is exported to PDF using the XtraReport.ExportToPdfAsync method.

JsonSourceCustomizationService

This service replaces the JsonDataSource.JsonSource value with a custom object that meets Blazor restrictions. This is because WebAssembly does not allow HTTP requests, and the report model may reference a JSON source with URI.

SimplifiedUriJsonSource

This class is a descendant of the UriJsonSource class and redirects HTTP requests to the JavaScipt segment of the application.

You can find the complete code in the sample project available in the DevExpress Examples repository on GitHub: Program.cs.

JavaScript Implementation

The main.mjs file is the core JS segment of the application. Replace its content with the following code:


// Import necessary modules.
import { dotnet } from '._framework/dotnet.js';
import fs from 'fs';
import { get as httpGet } from 'http';
import { get as httpsGet } from 'https';
import url from 'url'

// Configure .NET runtime for WASM execution.
const { setModuleImports, getAssemblyExports, getConfig } = await dotnet
    .withDiagnosticTracing(false)
    .create();
setModuleImports('main.mjs', { getJsonData });

// Retrieve the exported methods from the WASM part.
const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);

// Prepare the report model and related options.
const repx = fs.readFileSync('../reports/report1.repx');
const model = {
    reportXml: repx,
    exportOptions: {
        DocumentOptions: {
            Application: "WASM",
            Subject: "wasm integration"
        },
        PdfUACompatibility: "PdfUA1"
    }
}

// Export the report to PDF.
const result = {};
await exports.ReportExporter.ExportToPdfAsync(model, result);
const buffer = Buffer.from(result.pdf);
fs.writeFileSync('result.pdf', buffer);

// Define a method to fetch JSON data from a given URL.
function getJsonData(jsonUrl) {
    return new Promise((resolve) => {
        const fetchMethod = url.parse(jsonUrl).protocol === 'https:' ? httpsGet : httpGet;
        fetchMethod(jsonUrl, res => {
            let data = '';
            res.on('data', chunk => data += chunk);
            res.on('end', () => resolve(data));
        }).on('error', err => resolve(''));
    });
}

// Initiate the .NET runtime.
await dotnet.run();

The code in this file: 

  • Configures .NET runtime to run in WASM.
  • Imports the getJsonData() function to retrieve JSON files from URLs.
  • Calls and executes the ExportToPdfAsync method to generate a PDF document.
  • Saves the resulting PDF file using native Node.js functions.

View Results

To run the application, build the .NET app first, navigate to the result folder and then run the JavaScript application:


>dotnet build
>cd result
>node main.mjs

The application creates a new result.pdf file in the result directory.

Use DevExpress Web Document Viewer and Report Designer

In the previous section, we created a non-visual project that runs behind the scenes. However, we have a more complete project with UI controls that will give you more information about the features we added. It contains our Report Designer and Document Viewer for Blazor. A detailed explanation of how this project works is outside the scope of this post. The project is available on Github should you wish to explore our implementation further: Reporting for Web - Create Reports Using .NET Integration with Node.js in a WebAssembly App.

Follow the steps listed in the readme file, run the backend and client-side applications, and point your browser to the URL specified in the client-side app. The result will appear as follows:

Nodejs App - Web Report Designer

Conclusion

In this blog, we demonstrated how DevExpress Reporting for .NET can be integrated into a Node.js & WebAssembly environment - allowing you to create/customize reports with ease.

A complete sample project is available in the DevExpress Examples repository on GitHub: Reporting for Web - Create Reports Using .NET Integration with Node.js in a WebAssembly App.

Survey

Free DevExpress Products - Get Your Copy Today

The following free DevExpress product offers remain available. Should you have any questions about the free offers below, please submit a ticket via the DevExpress Support Center at your convenience. We'll be happy to follow-up.