javascriptc#interopwebassemblyumd

Is it possible to compile C# project into single-file UMD JavaScript library?


While Blazor provides C#/JS interop, it only works in browsers and is designed for SPA. Microsoft doesn't seem to plan adding support for other scenarios: https://github.com/dotnet/aspnetcore/issues/37910

Is it possible to use C# programs and libraries in JavaScript without dependency on DOM or other environment-specific APIs?


Solution

  • It's possible with a custom build of .NET WebAssembly runtime and environment-agnostic JavaScript wrapper.

    Here is a solution that uses such custom build to allow compiling C# project into UMD library, which can be used in browsers, node.js and custom restricted environments, such as VS Code's web extensions: https://github.com/Elringus/DotNetJS

    To use it, 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;
    
    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

    <!-- Import as a global 'dotnet' object via script tag. -->
    <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.`);
    })();