DotNetJS 0.17.0
See the version list below for details.
dotnet add package DotNetJS --version 0.17.0
NuGet\Install-Package DotNetJS -Version 0.17.0
<PackageReference Include="DotNetJS" Version="0.17.0" />
paket add DotNetJS --version 0.17.0
#r "nuget: DotNetJS, 0.17.0"
// Install DotNetJS as a Cake Addin #addin nuget:?package=DotNetJS&version=0.17.0 // Install DotNetJS as a Cake Tool #tool nuget:?package=DotNetJS&version=0.17.0
DotNetJS
The solution provides user-friendly workflow for consuming .NET C# programs and libraries in any JavaScript environment, be it web browsers, Node.js or custom restricted spaces, like web extensions for VS Code, where neither node modules nor browser APIs are available.
The solution is based on two main components:
- JavaScript. Consumes compiled C# assemblies and .NET runtime WebAssembly module to provide C# interoperability layer in JavaScript. The library is environment-agnostic — it doesn't depend on platform-specific APIs, like browser DOM or node modules and can be imported as CommonJS or ECMAScript module or consumed via script tag in browsers.
- DotNet. Provides JavaScript interoperability layer in C# and packs project output into single-file JavaScript library via MSBuild task. Produced library contains dotnet runtime initialized with the project assemblies and ready to be used as interoperability layer for the packaged C# project. Can optionally emit type definitions to bootstrap TypeScript development.
Quick Start
In C# project configuration file specify Microsoft.NET.Sdk.BlazorWebAssembly
SDK and import DotNetJS
NuGet package:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNetJS" Version="*"/>
</ItemGroup>
</Project>
To associate a JavaScript function with a C# method use JSFunction
attribute. To expose a C# method to JavaScript, use JSInvokable
attribute:
using System;
using DotNetJS;
using Microsoft.JSInterop;
namespace HelloWorld;
public partial class Program
{
// Entry point is invoked by the JavaScript runtime on boot.
public static void Main ()
{
// Invoking 'dotnet.HelloWorld.GetHostName()' JavaScript function.
var hostName = GetHostName();
// Writing to JavaScript host console.
Console.WriteLine($"Hello {hostName}, DotNet here!");
}
[JSFunction] // The interoperability code is auto-generated.
public static partial string GetHostName ();
[JSInvokable] // The method is invoked from JavaScript.
public static string GetName () => "DotNet";
}
Publish the project with dotnet publish
. A single-file dotnet.js
library will be produced under the "bin" directory. Consume the library depending on the environment:
Browser
<script src="dotnet.js"></script>
<script>
// Providing implementation for 'GetHostName' function declared in 'HelloWorld' C# assembly.
dotnet.HelloWorld.GetHostName = () => "Browser";
window.onload = async function () {
// Booting the DotNet runtime and invoking entry point.
await dotnet.boot();
// Invoking 'GetName()' C# method defined in 'HelloWorld' assembly.
const guestName = dotnet.HelloWorld.GetName();
console.log(`Welcome, ${guestName}! Enjoy your global space.`);
};
</script>
Node.js
// Import as CommonJS module.
const dotnet = require("dotnet");
// ... or as ECMAScript module in node v17 or later.
import dotnet from "dotnet.js";
// Providing implementation for 'GetHostName' function declared in 'HelloWorld' C# assembly.
dotnet.HelloWorld.GetHostName = () => "Node.js";
(async function () {
// Booting the DotNet runtime and invoking entry point.
await dotnet.boot();
// Invoking 'GetName()' C# method defined in 'HelloWorld' assembly.
const guestName = dotnet.HelloWorld.GetName();
console.log(`Welcome, ${guestName}! Enjoy your module space.`);
})();
Example Projects
Find the following sample projects in this repository:
- Hello World — Consume the produced library as a global import in browser, CommonJS or ES module in node.
- Web Extension — Consume the library in VS Code web extension, which works in both web and standalone versions of the IDE.
- React — A sample React app, which uses dotnet as backend. Features binaries side-loading, running dotnet on worker thread and mocking dotnet APIs in unit tests.
- Runtime Tests — Integration tests featuring various usage scenarios: async method invocations, interop with instances, sending raw byte arrays, streaming, etc.
A real-life usage of the solution can be found in https://github.com/Naninovel/Language. The project is an implementation of language server protocol that is used in VS Code extension: https://github.com/Naninovel/VSCode.
Events
To make a C# method act as event broadcaster for JavaScript consumers, annotate it with [JSEvent]
attribute:
[JSEvent]
public static partial string OnSomethingHappened (string payload);
— and consume it from JavaScript as follows:
dotnet.MyApp.OnSomethingHappened.subscribe(handleSomething);
dotnet.MyApp.OnSomethingHappened.unsubscribe(handleSomething);
function handleSomething (payload) {
}
When the method in invoked in C#, subscribed JavaScript handlers will be notified.
In TypeScript the event will have typed generic declaration corresponding to the event arguments.
Sideloading Binaries
By default, DotNetJS build task will embed project's DLLs and .NET WASM runtime to the generated JS library. While convenient and even required in some cases (eg, for VS Code web extensions), this also adds about 30% of extra size due to binary->base64 conversion of the embedded files.
To disable the embedding, set EmbedBinaries
build property to false:
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<EmbedBinaries>false</EmbedBinaries>
</PropertyGroup>
The dotnet.wasm
and solution's assemblies will be emitted in the build output directory. You will then have to provide them when booting:
const bootData = {
wasm: Uint8Array,
assemblies: [ { name: "Foo.dll", data: Uint8Array } ],
entryAssemblyName: "Foo.dll"
};
await dotnet.boot(bootData);
— this way the binary files can be streamed directly from server to optimize traffic and initial load time.
Use getBootUris()
function to get identifiers of all the resources required for boot. Below is an example on fetching the boot data; it assumes both wasm and assemblies are stored under /bin
directory on the remote server:
async function fetchBootData() {
const uris = getBootUris();
return {
wasm: await fetchBinary(uris.wasm),
assemblies: await Promise.all(uris.assemblies.map(fetchAssembly)),
entryAssemblyName: uris.entryAssembly
};
async function fetchBinary(name: string) {
const uri = `${process.env.PUBLIC_URL}/bin/${name}`;
return new Uint8Array(await (await fetch(uri)).arrayBuffer());
}
async function fetchAssembly(name: string) {
return { name, data: await fetchBinary(name) };
}
}
Find sideloading example in the React sample. Also, take a look at the build script, which automatically deploys the binaries to the react public directory after building the backend.
Running DotNet on Worker
When using dotnet as application backend, it makes sense to run the process on a background thread to prevent stalling the main (UI) thread. Enabling CreateWorker
build property will make dotnet run on worker thread and provide a mechanism to interact with the worker using the same APIs (via embedded comlink library):
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<CreateWorker>true</CreateWorker>
</PropertyGroup>
The dotnet web worker will start under the hood when the library is imported; the runtime can then be booted and used the same way. The only exception are the sync APIs: as it's not possible to communicate with web workers in blocking manner, all the methods will return promise, even if they do not return task on C# side.
When unit-testing applications with dotnet worker, define muteDotNetWorker
global property to disable the worker creation on library import. Check the React sample on setting the variable with jest.
Namespace Pattern
By default, all the generated JavaScript binding objects and TypeScript declarations are grouped under corresponding C# namespaces.
To override the generated namespaces, apply JSNamespace
attribute to the entry assembly of the C# program. The attribute expects pattern
and replacement
arguments, which are provided to Regex.Replace when building the generated namespace name.
For example, to transform Company.Product.Space
into Space
namespace, use the following pattern:
[assembly:JSNamespace(@"Company\.Product\.(\S+)", "$1")]
JSON Serializer Options
To override default JSON serializer options used for marshalling the interop data, use JS.Runtime.ConfigureJson
method before the program entry point is invoked. For example, below will add JsonStringEnumConverter
converter to allow serializing enums via strings:
static class Program
{
static Program () // Static constructor is invoked before 'Main'
{
JS.Runtime.ConfigureJson(options =>
options.Converters.Add(new JsonStringEnumConverter())
);
}
public static void Main () { }
}
Compiling Runtime
To compile and test the runtime run the following in order under JavaScript folder:
scripts/install-emsdk.sh
scripts/compile-runtime.sh
npm build
scripts/compile-test.sh
npm test
FAQ
I'm getting "An instance of analyzer Generator.SourceGenerator cannot be created" warning
You are probably using an older .NET SDK. Please download the latest version.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net6.0
- Microsoft.AspNetCore.Components.WebAssembly (>= 6.0.9)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on DotNetJS:
Package | Downloads |
---|---|
Plotly.WPF
Plotly.js in a WPF Control |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated | |
---|---|---|---|
0.23.4 | 1,788 | 5/14/2023 | |
0.23.3 | 871 | 2/21/2023 | |
0.23.2 | 288 | 2/21/2023 | |
0.23.1 | 554 | 1/27/2023 | |
0.23.0 | 344 | 1/27/2023 | |
0.22.0 | 452 | 1/21/2023 | |
0.21.1 | 382 | 12/23/2022 | |
0.21.0 | 462 | 12/5/2022 | |
0.20.0 | 404 | 12/4/2022 | |
0.19.0 | 394 | 11/24/2022 | |
0.18.0 | 828 | 11/20/2022 | |
0.17.1 | 529 | 10/15/2022 | |
0.17.0 | 480 | 10/10/2022 | |
0.16.0 | 554 | 10/1/2022 | |
0.15.1 | 500 | 9/1/2022 | |
0.15.0 | 554 | 8/24/2022 | |
0.14.0 | 599 | 7/1/2022 | |
0.13.8 | 574 | 6/6/2022 | |
0.13.7 | 455 | 6/4/2022 | |
0.13.5 | 453 | 6/4/2022 | |
0.13.4 | 480 | 6/4/2022 | |
0.13.3 | 444 | 6/3/2022 | |
0.13.2 | 480 | 5/24/2022 | |
0.13.1 | 491 | 5/23/2022 | |
0.13.0 | 489 | 5/22/2022 | |
0.12.0 | 487 | 5/21/2022 | |
0.11.0 | 506 | 5/21/2022 | |
0.10.1 | 468 | 5/19/2022 | |
0.10.0 | 546 | 5/1/2022 | |
0.9.2 | 463 | 4/29/2022 | |
0.9.1 | 491 | 4/17/2022 | |
0.9.0 | 461 | 4/8/2022 | |
0.8.2 | 525 | 3/22/2022 | |
0.8.1 | 475 | 3/9/2022 | |
0.8.0 | 488 | 2/20/2022 | |
0.7.3 | 628 | 2/16/2022 | |
0.6.3 | 517 | 2/12/2022 | |
0.6.2 | 497 | 2/9/2022 | |
0.5.4 | 494 | 2/8/2022 | |
0.5.3 | 602 | 2/1/2022 | |
0.5.2 | 528 | 1/17/2022 | |
0.5.1 | 485 | 1/13/2022 | |
0.5.0 | 514 | 1/13/2022 | |
0.4.1 | 353 | 1/10/2022 | |
0.4.0 | 359 | 12/24/2021 | |
0.3.13 | 337 | 12/23/2021 | |
0.3.12 | 655 | 12/4/2021 | |
0.3.11 | 924 | 12/1/2021 | |
0.3.8 | 367 | 11/30/2021 | |
0.2.5 | 890 | 11/21/2021 | |
0.2.0 | 501 | 11/19/2021 | |
0.1.0 | 375 | 11/18/2021 |