Incorporating .NET Functionality into JavaScript using WebAssembly
Improving browser-based computations with WebAssembly and .NET integration
What is WebAssembly?
WebAssembly or popularly known as WASM is an open web standard that allows efficient code execution in a binary format, similar to native code execution. It can be used with various languages like C, C++, Go, and C# .NET, enabling developers to compile their code into WASM files that can be downloaded and run in the browser.
Why Consider .NET in the Browser?
JavaScript undoubtedly reigns supreme in client-side development. However, what if your project demands server-side processing, such as file conversions or complex computations, that are more efficiently handled with .NET using C#? Simply making calls to the backend server might not be the most optimal solution. What if you could execute these operations directly within the browser using .NET? This is where WebAssembly comes into play, enabling you to harness the capabilities of .NET right in the client's browser environment. In this article, we delve into the potential of leveraging .NET with WebAssembly, revolutionizing the way we approach web development.
Lets get started
To unleash the power of .NET in your browser, you'll need to ensure you have the .NET 7 or latest, installed on your system. Additionally, make sure to equip your environment with the necessary tools, such as the wasm-tools workload, to seamlessly integrate WebAssembly into your development workflow.
wasm-tools can be installed with following commands
dotnet workload install wasm-tools
Create a new dotnet console application
dotnet new console
Now to convert our console application to build WebAssembly bindings on compilation we need to below tags in <PropertyGroup> tag in csproj file, also we need to create a main.js file in the same project.
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<WasmMainJSPath>main.js</WasmMainJSPath>
In Program.cs, add the following code
using System.Runtime.InteropServices.JavaScript;
using System.Text.Json;
using System.Text.Json.Serialization;
Console.WriteLine(""); //work around for main is not defined error on code compilation
public partial class JSBridge
{
private static Employee employee { get; set; }
[JSImport("globalThis.console.log")]
internal static partial void Log([JSMarshalAs<JSType.String>] string message);
[JSExport]
internal static string GetEmployee()
{
string jsonString = JsonSerializer.Serialize(employee, SourceGenerationContext.Default.Employee);
Log(jsonString);//Logging message log to console
return jsonString;
}
[JSExport]
internal static void SetEmployee(int _id, string _name, int _age, bool _isActive)
{
Log($"{nameof(Employee.EmployeeId)}: {_id}, {nameof(Employee.EmployeeName)}: {_name}, {nameof(Employee.EmployeeAge)}: {_age}, {nameof(Employee.IsActive)}: {_isActive},");
employee = new Employee() { EmployeeId = _id, EmployeeName = _name, EmployeeAge = _age, IsActive = _isActive };
}
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Employee))]
internal partial class SourceGenerationContext : JsonSerializerContext
{
}
public class Employee
{
public int EmployeeId { get; set; }
public string EmployeeName { get; set; }
public int EmployeeAge { get; set; }
public bool IsActive { get; set; }
}
Information related to JSExport and JSImport can be found here and in main.js we need to create a wrapper to access the .NET code using JavaScript.
import { dotnet } from "./_framework/dotnet.js";
let exportsPromise = null;
const createRuntimeAndGetExports = async () => {
const { getAssemblyExports, getConfig } = await dotnet.create();
const config = getConfig();
return await getAssemblyExports(config.mainAssemblyName);
};
export async function getEmployee() {
if (!exportsPromise) {
exportsPromise = createRuntimeAndGetExports();
}
const exports = await exportsPromise;
return exports.JSBridge.GetEmployee();
}
export async function setEmployee(id, name, age, isActive) {
if (!exportsPromise) {
exportsPromise = createRuntimeAndGetExports();
}
const exports = await exportsPromise;
return exports.JSBridge.SetEmployee(id,name,age,isActive);
}
Now our C# code and JavaScript wrapper is ready to publish, it can be done from Visual Studio itself by Publish or running below command.
dotnet publish -c Release
On invoking the command .NET code will be converted to WebAssembly binding which can be triggered by our main.js exports. AppBundle folder will be created with the similar files. Package.json file can be created if we need to use it as npm module.
Now well be using the .NET code in a JavaScript, for it we can create a basic Node.js application which uses express serving static HTML file.
Add the following code to the index.js file, also create a new folder public and subfolder dotnet and copy the contents of AppBundle folder which were generated on publishing .NET wasm project.
const express = require("express");
const path=require("path");
const app = express();
app.use(express.static(path.join(__dirname, 'public')));
app.get("/", async (req, res) => {
res.sendFile("index.html",{root:__dirname});
});
app.listen(8000, () => {
console.log("Server started on http://localhost:8000/");
});
and the last step is to create an index.html file which will invoke the .NET code.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.form-container {
display: flex;
}
.btn {
display: inline-block;
padding: 2%;
width: 100%;
margin-top: 5px;
}
</style>
</head>
<body>
<h2>Employee Information</h2>
<table>
<tbody>
<tr>
<td>Employee Id</td>
<td><input id="txtEmployeeId" type="number" /></td>
</tr>
<tr>
<td>Employee Name</td>
<td><input id="txtEmployeeName" type="text" /></td>
</tr>
<tr>
<td>Employee Age</td>
<td><input id="txtEmployeeAge" type="number" /></td>
</tr>
<tr>
<td>Is Active</td>
<td><input id="chkIsActive" type="checkbox" /></td>
</tr>
<tr>
<td></td>
<td>
<button class="btn" onclick="btnSetEmployee_Click()">
Set Employee
</button>
</td>
</tr>
<tr>
<td></td>
<td>
<button class="btn" onclick="btnGetEmployee_Click()">
Get Employee
</button>
</td>
</tr>
</tbody>
</table>
<script>
const btnSetEmployee_Click = async () => {
let employeeId = Number(document.getElementById("txtEmployeeId").value);
let employeeName = document.getElementById("txtEmployeeName").value;
let employeeAge = Number(
document.getElementById("txtEmployeeAge").value
);
let isActive = document.getElementById("chkIsActive").checked;
if (employeeId && employeeName && employeeAge && isActive) {
import("/dotnet/main.js").then(({ setEmployee }) => {
setEmployee(employeeId, employeeName, employeeAge, isActive).then(()=>{
alert('Success');
})
});
} else {
alert("Invalid data entered");
}
};
const btnGetEmployee_Click = async () => {
import("/dotnet/main.js").then(({ getEmployee }) => {
getEmployee().then((result)=>{
if(result===null){
alert('no data present');
return;
}
alert(result);
});
});
};
</script>
</body>
</html>
on running the node.js code html file will be displayed, on Setting employee data we are invoking the C# code which internally log data to console in browser as we have imported console.log functionality using JSImport, similarly on getting employee data we are reading serialized string data from the Employee class object we have created earlier in the code.
You can find the code used in this blog on my Github here and do check out my other projects and connect with on LinkedIn.
https://www.linkedin.com/in/nilaybarot/
Additional Resources
https://learn.microsoft.com/en-us/aspnet/core/client-side/dotnet-interop?view=aspnetcore-8.0