Without Blazor WebAssembly, develop a web site that compiles and runs C# code on browser

Zack Yang
9 min readFeb 10, 2023

--

In this article, I will share with you on how to “ build a tool to compile and run C# code in the browser”. The core technology is to write WebAssembly and Roslyn technology in C# without relying on the Blazor framework.

1. Why such a tool is needed?

For programming novices, the installation and configuration of the development environment is a headache, if we can let beginners do not do any installation and configuration, but directly open the browser and write, run code on the browser, then this will greatly reduce the threshold of learning programming novices.

There are already some websites that can enabled learners to write and run C# code online, these websites have the following two approaches:

Approach 1: The code is delivered from the front end to the back-end server, where it is compiled, run, and displayed to the front end. The disadvantage of this approach is that it cannot complete complex input/output, interface interaction, etc.

Approach 2: Writing WebAssembly using Mono technology. The downside is that the C# syntax is not followed up in a timely manner and some new C# syntax is not supported.

Therefore, developing a tool that compiles and runs C# code on the browser side and supports the latest C# syntax is important. To develop such a tool, WebAssembly is an unavoidable technology.

2. What is WebAssembly?

While traditional front-end development uses JavaScript, WebAssembly lets you write programs that run in the browser using other programming languages. Because WebAssembly is standard in modern browsers, no additional plug-in is required to run the WebAssembly program in the browser. Major programming languages such as Java, Go, Python, and others now all support compilation to WebAssembly.

3. Disadvantage of Blazor WebAssembly

The Blazor WebAssembly technology in.NET compiles C# code into WebAssembly to run on the browser side. But traditional Blazor WebAssembly is a very intrusive framework, meaning that the entire system must be developed using C# technology, rather than just one component using C# code and the rest using traditional JavaScript. Of course, using Microsoft.AspNetCore.Components.CustomElements, we can only leave a placeholder on the UI for Blazor WebAssembly, but this way is still very heavy, we can’t implement a lightweight component like “just write a function in C#”, that is, an non-intrusive, lightweight WebAssembly component in C# with low dependencies.

4. Without Blazor WebAssembly, write WebAssembly in C#

Starting with.NET 6, we can write a lightweight WebAssembly in C# that uses only the basic runtime environment provided by Blazor, rather than introducing the entire Blazor WebAssembly.

Now, I will demonstrate the use of this technique with a simple example of calculating the sum of two numbers in C#. Of course, this is just a simple demonstration, something that would never be done in C# in a real project. The following projects use.NET 7 for demonstration purposes. Other versions may be used slightly differently.

a) Create a .NET “class library” project and then install the following two components via Nuget: Microsoft.AspNetCore.Components.WebAssembly, Microsoft.AspNetCore.Components.WebAssembly.DevServer. Then change the attribute ‘Sdk’ of the *.csproj file to “Microsoft.NET.Sdk.BlazorWebAssembly”.

The modified file looks like Code 1.

Code 1 *csproj file

<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.2" />
</ItemGroup>
</Project>

b) Create a file Program.cs in the library project, as shown in Code 2.

Code 2 Program.cs

using Microsoft.JSInterop;

namespace Demo1
{
public class Program
{
private static async Task Main(string[] args)
{
}

[JSInvokable]
public static int Add(int i,int j)
{
return i + j;
}
}
}

The body of Main method is currently empty, but cannot be omitted. The [JSInvokable] on the Add method means that the method can be called by JavaScript, which means that it’s a method exported by Web Assembly.

c) Build the generated project. Generated Web Assembly and related files are in the “_framework” folder in the “wwwroot” folder under the output folder.

d) Create a front-end project using any front-end technology you like. Instead of using any front-end framework, I’ll write the front-end project in plain HTML+Javascript.

First, copy the _framework folder generated in the previous step into the root folder of the front-end project.

Then we write the index.html file shown in code 3.

Code 3 index.html

<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body></body>
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
<script>
window.onload = async function () {
await Blazor.start();
const r = await DotNet.invokeMethodAsync(
'Demo1',//Assembly name
'Add',//the method decorated with [JSInvokable]
666,//some arguments
333
);
alert(r);
};
</script>
</html>

Let me explain the above code,<script src=”_framework/blazor.webassembly.js” autostart=”false”></script> is for importing related files. Blazor.start() is for start Blazor runtime; DotNet.invokeMethodAsync is for invoking a method of the WebAssembly, and the first argument is the name of the called assembly, the second argument is the name of the called method, and the next arguments are the values passed to the called method as arguments.

As can be seen, we are using WebAssembly as a component here, with no other special requirements for the page. So this component can be used in any front-end framework, and can also be compatible with other front-end libraries.

Finally, let’s run the front-end project. Because Blazor generates *.blat, *.dll etc files that may not be accepted by the Web server by default, please make sure to configure Mimetyps mappings ffor the following format on your web server: .pdb, .blat, .dat, .dll, .json, .wasm, .woff, .woff2.

The Web server I used for testing here is IIS, so you can create the Web.config file as shown in the root folder of the website, as shown in code 4. For developers using other Web servers, please refer to the manual of the Web server used to configure MimeType mappings.

Code 4 Web.config

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<staticContent>
<remove fileExtension=".pdb" />
<remove fileExtension=".blat" />
<remove fileExtension=".dat" />
<remove fileExtension=".dll" />
<remove fileExtension=".json" />
<remove fileExtension=".wasm" />
<remove fileExtension=".woff" />
<remove fileExtension=".woff2" />
<mimeMap fileExtension=".pdb" mimeType="application/octet-stream" />
<mimeMap fileExtension=".blat" mimeType="application/octet-stream" />
<mimeMap fileExtension=".dll" mimeType="application/octet-stream" />
<mimeMap fileExtension=".dat" mimeType="application/octet-stream" />
<mimeMap fileExtension=".json" mimeType="application/json" />
<mimeMap fileExtension=".wasm" mimeType="application/wasm" />
<mimeMap fileExtension=".woff" mimeType="application/font-woff" />
<mimeMap fileExtension=".woff2" mimeType="application/font-woff" />
</staticContent>
</system.webServer>
</configuration>

e) Access the index.html in the Web server on rowser, if you see the pop-up window as shown in Figure 1, it means that Javascript has successfully called the Add method written in C#.

Figure 1 Message popped

5. Scenarios of writing WebAssembly in C#

WebAssembly written in C# by default accounts for a large amount of traffic, around 30MB. We can use BlazorLazyLoad, enable Brotli algorithm and other ways to reduce traffic below 5MB. Please search online for details.

In my opinion, writing WebAssembly in C# includes, but is not limited to, the following scenarios.

Scenario 1: Reuse some. NET component or C# code. The existing .NET components or C# code can also be rewritten in Javascript, but this adds extra work. These components can be reused directly through WebAssembly. For example, I have used a PE file parsing Nuget package for back-end development, and I can continue to use the package directly in WebAssembly to process PE files on the front end.

Scenario 2: using some WebAssembly components. Because programs written in languages such as C/C++ can be ported to the WebAssembly version, many classic C/C++ developed software can continue to be used on the front end as well. For example, FFMpeg for audio and video processing already has a WebAssembly version, so you can call it in C# for audio and video processing. For example, the famous computer vision library OpenCV has also been ported to WebAssembly, so we can also use C# in the front end for image recognition, image processing and other image operations. WebAssembly is ideal for developing tools such as “online image processing, online audio and video, online games”.

Scenario 3: Develop some complex front-end components. We know that Javascript often falls short when it comes to developing complex projects, and even Typescript isn’t fundamentally better than Javascript. In contrast, C# and others are more suitable, so some very complex front-end components may be more appropriate to be writen in C# for WebAssembly.

In the scenarios above, you can develop only some of the components in C#, and the rest can continue to be developed in Javascript, so we can take advantage of each language.

6. Invoke methods of Javascript in C#

When writing WebAssembly, we might need to call methods of Javascript in C#. We can invoke asynchronous and synchronous methods in Javascript with IJSRuntime and IJSInProcessRuntime respectively.

Create a library project, named Demo2, and first configure the project as described in Section 4 above.

The code for index.html and Program.cs is shown in codes 5 and 6.

Code 5 index.html

<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<div id="divDialog" style="display:none">
<div id="divMsg"></div>
<input type="text" id="txtValue" />
<button id="btnClose">close</button>
</div>
<ul id="ulMsgs">
</ul>
</body>
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
<script>
function showMessage(msg) {
const divDialog = document.getElementById("divDialog");
divDialog.style.display = "block";
const divMsg = document.getElementById("divMsg");
divMsg.innerHTML = msg;
const txtValue = document.getElementById("txtValue");
const btnClose = document.getElementById("btnClose");
return new Promise(resolve => {
btnClose.onclick = function () {
divDialog.style.display = "none";
resolve(txtValue.value);
};
});
}
function appendMessage(msg) {
const li = document.createElement("li");
li.innerHTML = msg;
const ulMsgs = document.getElementById("ulMsgs");
ulMsgs.appendChild(li);
}
window.onload = async function () {
await Blazor.start();
await DotNet.invokeMethodAsync('Demo2','Count');
};
</script>
</html>

Code 6 Program.cs

using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.JSInterop;

namespace Demo2;

public class Program
{
private static IJSRuntime js;

private static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
var host = builder.Build();
js = host.Services.GetRequiredService<IJSRuntime>();
await host.RunAsync();
}

[JSInvokable]
public static async Task Count()
{
string strCount = await js.InvokeAsync<string>("showMessage","Input Count");
for(int i=0;i<int.Parse(strCount);i++)
{
((IJSInProcessRuntime)js).InvokeVoid("appendMessage",i);
}
((IJSInProcessRuntime)js).InvokeVoid("alert", "Done");
}
}

The appendMessage() method defined in Index.html is a synchronous method for attaching a given message to “ul”; showMessage() is an asynchronous method that displays an HTML-simulated input dialog that closes when the user clicks the [Close] button and returns what the user entered. This operation involves the concept of promises in Javascript, if you’re not familiar with this,please check out online resources.

In Program.cs, we get the IJSRuntime service used to invoke Javascript code in the Main method. IJSRuntime interface provides InvokeAsync and InvokeVoidAsync methods, which are used to asynchronously invoke JavaScript methods with and without a return value. If you want to call methods in Javascript synchronously, you need to cast IJSRuntime to IJSInProcessRuntime, and then call its InvokeVoid and Invoke methods. The Count() method labeled with [JSInvokable] is asynchronous, and asynchronous methods in C# are called in Javascript the same way.

7. Compile C# code at runtime: Roslyn

Roslyn is used to compile C# code at runtime, and Roslyn supports use in WebAssembly, so we use this component to compile C# code here. The usage of Roslyn is widely available on the Internet and I won’t go into detail here.

The only caveat is that Roslyn builds concurrently by default, aas it speeds up the compilation. However, due to the limitations of the browser sandbox environment, WebAssembly does not support concurrent build, so if you use the default Roslyn compilation setting, “System.PlatformNotSupportedException: Cannot wait on monitors on this runtime” will be thrown when the compilation operation is performed. Therefore, please set concurrentBuild=false for CSharpCompilationOptions, as shown in Code 7.

Code 7 disable concurrentBuild

var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, concurrentBuild: false);
var scriptCompilation = CSharpCompilation.CreateScriptCompilation(
"main.dll", syntaxTree, options: compilationOptions).AddReferences(references);

8. Replace the default implementation of the Console class

Due to the limitations of the browser sandbo, not all. NET classes are callable, or their functionality is limited. For example, when calling IO classes in WebAssembly, you can’t read or write files on the user’s disk at will, only because of the security restrictions of the browser sandbox environment. In WebAssembly, for example, you can call the HttpClient class to make Http requests, but again, the cross-domain access of the browser is limited. WebAssembly is powerful, but no matter how powerful it is, it doesn’t break the limits of sandbox.

In this tool for compiling and running C# code online, I want the user to be able to use Console.WriteLine() and Console.ReadLine() to interact with the user on output and input. However, in Web Assembly, Console.Writeline () writes message in the console of developer tools, equivalent to executing console.log() in JavaScript; Console.ReadLine() is not available in Web Assembly. So I wrote a Console class with the same name and provided implementations of the WriteLine() and ReadLine() methods using the JavaScript alert() and prompt() functions, respectively. When compiling user-written code with Roslyn, I use the assembly of my custom Console class instead of System.Console.dll, so that the Console class in the user-written code calls my custom class.

9. Demo and Github repository

I have deployed the project online, and it can be accessed on https://block.youzack.com/editor.html, as shown in Figure 2.

Figure 2 Result

Write C# code in the code editor, then click the [Run] button. If the code has errors, the interface will also display detailed compilation error information.

The Github repository of the project is: https://github.com/yangzhongke/WebCSC

10. Conclusion

Since.NET 6, it has been possible to move away from the traditional and intrusive Blazor WebAssembly framework and write lightweight, non-intrusive WebAssembly applications in C# that can be developed in conjunction with Javascript. Let the project in the development efficiency, engineering management and other aspects to achieve better results.

This article introduced you to developing a non-intrusive WebAssembly component in C# and shared an open-source project for writing, compiling, and running C# development in the browser.

--

--