Using .NET code from JavaScript using WebAssembly
Blazor WebAssembly allows running a .NET web application in a browser. Starting with .NET 7, you can easily run any .NET method from JavaScript without needing the whole Blazor framework. Let's see how to run a .NET method from JavaScript!
First, you need to install the WASM workload, so you can publish 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:
<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>
Then, you can edit the Program.cs
file with a static method decorated with the [JSExport]
attribute. This attribute indicates the method is accessible from JS.
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.
// 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, you need to create an html file to load the JS module.
<!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:
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. But, you need to compress the output using Brotli to get the actual result. Also, by default, the file dotnet.timezones.blat
is included. I could not find a way to remove it. So, I've edited the mono-config.json
file directly to remove it. I also removed System.Private.Uri.dll
(26kB) which is not used in this sample. I'm not sure why the linker cannot remove this file automatically.
The final size is 819kB. Here's 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 big. Other DLLs, are small and are pay-for-use thanks to the linker.
Do you have a question or a suggestion about this post? Contact me!