Using .NET code from JavaScript using WebAssembly

 
 
  • Gérald Barré

Blazor WebAssembly enables running a .NET web application in a browser. Starting with .NET 7, you can run any .NET method from JavaScript without needing the full Blazor framework. Here is how to set it up.

First, install the WASM workload to enable publishing the app later:

dotnet workload install wasm-tools

Then, you can create a new console app:

dotnet new console

Let's edit the csproj file to add support for WASM:

Sample.csproj (csproj (MSBuild project file))
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>

    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    <WasmMainJSPath>main.js</WasmMainJSPath>

    <!-- JSExport requires unsafe code -->
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>

    <!-- Reduce output size -->
    <InvariantGlobalization>true</InvariantGlobalization>
    <WasmEmitSymbolMap>false</WasmEmitSymbolMap>
  </PropertyGroup>

  <!-- Copy extra files when publishing the app -->
  <ItemGroup>
    <WasmExtraFilesToDeploy Include="index.html" />
  </ItemGroup>
</Project>

</Project>

Next, edit the Program.cs file to add a static method decorated with the [JSExport] attribute. This attribute marks the method as accessible from JavaScript.

C#
using System.Runtime.InteropServices.JavaScript;

// Create a "Main" method. This is required by the tooling.
return;

public partial class Sample
{
    // Make the method accessible from JS
    [JSExport]
    internal static int Add(int a, int b)
    {
        return a + b;
    }
}

The [JSExport] attribute instructs the Source Generator to generate the interop code for this method. If you are curious, you can check the generated code in Visual Studio:

Then, you can create a new file named main.js at the root of the project. The file name must match the property <WasmMainJSPath> from the csproj.

main.js (JavaScript)
// Set up the .NET WebAssembly runtime
import { dotnet } from './dotnet.js'

// Get exported methods from the .NET assembly
const { getAssemblyExports, getConfig } = await dotnet
    .withDiagnosticTracing(false)
    .create();

const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);

// Access JSExport methods using exports.<Namespace>.<Type>.<Method>
const result = exports.Sample.Add(1, 2);

// Display the result of the .NET method
document.getElementById("out").innerHTML = `Result: ${result}`;

Finally, create an HTML file to load the JavaScript module.

index.html (HTML)
<!DOCTYPE html>
<html>
  <head>
    <title>Sample</title>
    <meta charset="UTF-8">
  </head>
  <body>
    <div id="out"></div>
    <script type="module" src="main.js"></script>
  </body>
</html>

The source code is ready. The final step is to publish the application:

Shell
dotnet publish --configuration Release

The result is available in the folder bin/Release/net7.0/browser-wasm/AppBundle. You can open the index.html file from this folder to see the result.

Download the code from this post: sample-dotnet-webassembly.zip.

#Application size

The uncompressed publish folder is 2.81MB. You need to compress the output using Brotli to get the actual size. By default, the file dotnet.timezones.blat is included with no built-in option to exclude it, so the mono-config.json file can be edited directly to remove it. System.Private.Uri.dll (26kB) was also removed since it is not used in this sample; it is unclear why the linker does not remove it automatically.

The final size is 819kB. Here is the breakdown:

Payload size (brotli): ~819KB

  • dotnet.js ⇒ 54kB
  • dotnet.wasm ⇒ 342kB
  • SampleProject.dll: 3kB
  • System.Private.CoreLib.dll: 407kB
  • System.Runtime.dll: 2kB
  • System.Runtime.InteropServices.JavaScript.dll: 14kB

The baseline (dotnet.js, dotnet.wasm, and System.Private.CoreLib.dll) is quite large. Other DLLs are small and included on a pay-for-use basis thanks to the linker.

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?