JavaScript [JSImport]/[JSExport] interop with a WebAssembly Browser App project (original) (raw)

Important

This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.

For the current release, see the .NET 9 version of this article.

This article explains how to setup a WebAssembly Browser App project to run .NET from JavaScript (JS) using JS [JSImport]/[JSExport] interop. For additional information and examples, see JavaScript `[JSImport]`/`[JSExport]` interop in .NET WebAssembly.

For additional guidance, see the Configuring and hosting .NET WebAssembly applications guidance in the .NET Runtime (dotnet/runtime) GitHub repository.

Existing JS apps can use the expanded client-side WebAssembly support to reuse .NET libraries from JS or to build novel .NET-based apps and frameworks.

These approaches are appropriate when you only expect the Blazor app to run on WebAssembly (WASM). Libraries can make a runtime check to determine if the app is running on WASM by calling OperatingSystem.IsBrowser.

Prerequisites

.NET SDK (latest version)

Install the wasm-tools workload in an administrative command shell, which brings in the related MSBuild targets:

dotnet workload install wasm-tools

The tools can also be installed via Visual Studio's installer under the ASP.NET and web development workload in the Visual Studio installer. Select the .NET WebAssembly build tools option from the list of optional components.

Optionally, install the wasm-experimental workload, which adds the following experimental project templates:

After installing the workload, these new templates can be selected when creating a new project. This workload isn't required if you plan to integrate JS [JSImport]/[JSExport] interop into an existing JS app.

dotnet workload install wasm-experimental

The templates can also be installed from the Microsoft.NET.Runtime.WebAssembly.Templates NuGet package with the following command:

dotnet new install Microsoft.NET.Runtime.WebAssembly.Templates

For more information, see the Experimental workload and project templates section.

Namespace

The JS interop API described in this article is controlled by attributes in the System.Runtime.InteropServices.JavaScript namespace.

Project configuration

To configure a project (.csproj) to enable JS interop:

<TargetFramework>{TARGET FRAMEWORK}</TargetFramework>  

.NET 7 (net7.0) or later is supported.

<AllowUnsafeBlocks>true</AllowUnsafeBlocks>  

The following is an example project file (.csproj) after configuration. The {TARGET FRAMEWORK} placeholder is the target framework:

<Project Sdk="Microsoft.NET.Sdk.WebAssembly">

  <PropertyGroup>
    <TargetFramework>{TARGET FRAMEWORK}</TargetFramework>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>

</Project>
<TargetFramework>net7.0</TargetFramework>  

.NET 7 (net7.0) or later is supported.

<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>  
<OutputType>Exe</OutputType>  
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>  
<WasmMainJSPath>main.js</WasmMainJSPath>  

Example project file (.csproj) after configuration:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    <OutputType>Exe</OutputType>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <WasmMainJSPath>main.js</WasmMainJSPath>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

JavaScript interop on WASM

APIs in the following example are imported from dotnet.js. These APIs enable you to set up named modules that can be imported into your C# code and call into methods exposed by your .NET code, including Program.Main.

Important

"Import" and "export" throughout this article are defined from the perspective of .NET:

In the following example:

JS module:

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

const { setModuleImports, getAssemblyExports, getConfig, runMain } = await dotnet
  .withApplicationArguments("start")
  .create();

setModuleImports('main.js', {
  dom: {
    setInnerText: (selector, time) => 
      document.querySelector(selector).innerText = time
  }
});

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

document.getElementById('reset').addEventListener('click', e => {
  exports.StopwatchSample.Reset();
  e.preventDefault();
});

const pauseButton = document.getElementById('pause');
pauseButton.addEventListener('click', e => {
  const isRunning = exports.StopwatchSample.Toggle();
  pauseButton.innerText = isRunning ? 'Pause' : 'Start';
  e.preventDefault();
});

await runMain();
import { dotnet } from './_framework/dotnet.js'

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

setModuleImports('main.js', {
  window: {
    location: {
      href: () => globalThis.window.location.href
    }
  }
});

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

document.getElementById('out').innerHTML = text;
await dotnet.run();
import { dotnet } from './dotnet.js'

const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`);

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

setModuleImports("main.js", {
  window: {
    location: {
      href: () => globalThis.window.location.href
    }
  }
});

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

document.getElementById("out").innerHTML = text;
await dotnet.run();

To import a JS function so it can be called from C#, use the new JSImportAttribute on a matching method signature. The first parameter to the JSImportAttribute is the name of the JS function to import and the second parameter is the name of the module.

In the following example, the dom.setInnerText function is called from the main.js module when SetInnerText method is called:

[JSImport("dom.setInnerText", "main.js")]
internal static partial void SetInnerText(string selector, string content);

In the following example, the window.location.href function is called from the main.js module when GetHRef method is called:

[JSImport("window.location.href", "main.js")]
internal static partial string GetHRef();

In the imported method signature, you can use .NET types for parameters and return values, which are marshalled automatically by the runtime. Use JSMarshalAsAttribute to control how the imported method parameters are marshalled. For example, you might choose to marshal a long as System.Runtime.InteropServices.JavaScript.JSType.Number or System.Runtime.InteropServices.JavaScript.JSType.BigInt. You can pass Action/Func callbacks as parameters, which are marshalled as callable JS functions. You can pass both JS and managed object references, and they are marshaled as proxy objects, keeping the object alive across the boundary until the proxy is garbage collected. You can also import and export asynchronous methods with a Task result, which are marshaled as JS promises. Most of the marshalled types work in both directions, as parameters and as return values, on both imported and exported methods.

For additional type mapping information and examples, see JavaScript `[JSImport]`/`[JSExport]` interop in .NET WebAssembly.

Functions accessible on the global namespace can be imported by using the globalThis prefix in the function name and by using the [JSImport] attribute without providing a module name. In the following example, console.log is prefixed with globalThis. The imported function is called by the C# Log method, which accepts a C# string message (message) and marshalls the C# string to a JS String for console.log:

[JSImport("globalThis.console.log")]
internal static partial void Log([JSMarshalAs<JSType.String>] string message);

To export a .NET method so it can be called from JS, use the JSExportAttribute.

In the following example, each method is exported to JS and can be called from JS functions:

[JSExport]
internal static bool Toggle()
{
    if (stopwatch.IsRunning)
    {
        stopwatch.Stop();
        return false;
    }
    else
    {
        stopwatch.Start();
        return true;
    }
}

[JSExport]
internal static void Reset()
{
    if (stopwatch.IsRunning)
        stopwatch.Restart();
    else
        stopwatch.Reset();

    Render();
}

[JSExport]
internal static bool IsRunning() => stopwatch.IsRunning;

In the following example, the Greeting method returns a string that includes the result of calling the GetHRef method. As shown earlier, the GetHref C# method calls into JS for the window.location.href function from the main.js module. window.location.href returns the current page address (URL):

[JSExport]
internal static string Greeting()
{
    var text = $"Hello, World! Greetings from {GetHRef()}";
    Console.WriteLine(text);
    return text;
}

Experimental workload and project templates

To demonstrate the JS interop functionality and obtain JS interop project templates, install the wasm-experimental workload:

dotnet workload install wasm-experimental

The wasm-experimental workload contains two project templates: wasmbrowser and wasmconsole. These templates are experimental at this time, which means the developer workflow for the templates is evolving. However, the .NET and JS APIs used in the templates are supported in .NET 8 and provide a foundation for using .NET on WASM from JS.

The templates can also be installed from the Microsoft.NET.Runtime.WebAssembly.Templates NuGet package with the following command:

dotnet new install Microsoft.NET.Runtime.WebAssembly.Templates

Browser app

You can create a browser app with the wasmbrowser template from the command line, which creates a web app that demonstrates using .NET and JS together in a browser:

dotnet new wasmbrowser

Alternatively in Visual Studio, you can create the app using the WebAssembly Browser App project template.

Build the app from Visual Studio or by using the .NET CLI:

dotnet build

Build and run the app from Visual Studio or by using the .NET CLI:

dotnet run

Alternatively, install and use the dotnet serve command:

dotnet serve -d:bin/$(Configuration)/{TARGET FRAMEWORK}/publish

In the preceding example, the {TARGET FRAMEWORK} placeholder is the target framework moniker.

Node.js console app

You can create a console app with the wasmconsole template, which creates an app that runs under WASM as a Node.js or V8 console app:

dotnet new wasmconsole

Alternatively in Visual Studio, you can create the app using the WebAssembly Console App project template.

Build the app from Visual Studio or by using the .NET CLI:

dotnet build

Build and run the app from Visual Studio or by using the .NET CLI:

dotnet run

Alternatively, start any static file server from the publish output directory that contains the main.mjs file:

node bin/$(Configuration)/{TARGET FRAMEWORK}/{PATH}/main.mjs

In the preceding example, the {TARGET FRAMEWORK} placeholder is the target framework moniker, and the {PATH} placeholder is the path to the main.mjs file.

Additional resources