Compiling and running C # and Blazor inside a browser
Introduction
If you are a web developer and are developing for a browser, then you are definitely familiar with JS, which can be executed inside a browser. There is an opinion that JS is not very suitable for complex calculations and algorithms. And although in recent years JS has made a big leap in performance and breadth of use, many programmers continue to dream of launching a system language inside the browser. In the near future, the game may change thanks to WebAssembly.
Microsoft is not standing still and is actively trying to port .NET to WebAssembly. As one of the results, we received a new framework for customer development - Blazor. It’s not quite clear yet whether Blazor can be faster than modern JS frameworks like React, Angular, Vue at the expense of WebAssembly. But it definitely has a big advantage - development in C #, as well as the whole .NET Core world can be used inside the application.
Compiling and running C # in Blazor
The process of compiling and executing such a complex language as C # is a complex and time-consuming task. А можно ли внутри браузера скомпилировать и выполнить С#?
- It depends on the capabilities of the technology (or rather, the core). However, Microsoft, as it turned out, has already prepared everything for us.
First, create a Blazor app.
After that, you need to install Nuget - a package for analyzing and compiling C #.
Install-Package Microsoft.CodeAnalysis.CSharp
Prepare the start page.
@page "/"
@inject CompileService service
<h1>Compile and Run C# in Browser</h1><div><divclass="form-group"><labelfor="exampleFormControlTextarea1">C# Code</label><textareaclass="form-control"id="exampleFormControlTextarea1"rows="10"bind="@CsCode"></textarea></div><buttontype="button"class="btn btn-primary"onclick="@Run">Run</button><divclass="card"><divclass="card-body"><pre>@ResultText</pre></div></div><divclass="card"><divclass="card-body"><pre>@CompileText</pre></div></div></div>
@functions
{
string CsCode { get; set; }
string ResultText { get; set; }
string CompileText { get; set; }
public async Task Run()
{
ResultText = await service.CompileAndRun(CsCode);
CompileText = string.Join("\r\n", service.CompileLog);
this.StateHasChanged();
}
}
First you need to parse the string into an abstract syntax tree. Since in the next step we will be compiling Blazor components, we need the latest ( LanguageVersion.Latest
) version of the language. For this, Roslyn for C # has a method:
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(code, new CSharpParseOptions(LanguageVersion.Latest));
Already at this stage, you can detect gross compilation errors by reading the parser diagnostics.
foreach (var diagnostic in syntaxTree.GetDiagnostics())
{
CompileLog.Add(diagnostic.ToString());
}
Next, compile Assembly
to a binary stream.
CSharpCompilation compilation = CSharpCompilation.Create("CompileBlazorInBlazor.Demo", new[] {syntaxTree},
references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
using (MemoryStream stream = new MemoryStream())
{
EmitResult result = compilation.Emit(stream);
}
Note that you need to get references
- a list of metadata of the connected libraries. But reading these files along the way Assembly.Location
did not work, because there is no file system in the browser. Perhaps there is a more effective way to solve this problem, but the goal of this article is a conceptual opportunity, so we will download these libraries again via Http and do it only the first time the compilation starts.
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
references.Add(
MetadataReference.CreateFromStream(
await this._http.GetStreamAsync("/_framework/_bin/" + assembly.Location)));
}
From EmitResult
you can find out whether the compilation was successful, as well as get diagnostic errors.
Now you need to load Assembly
into the current AppDomain
and execute the compiled code. Unfortunately, there is no possibility to create several inside the browser AppDomain
, so it’s safe to download and upload Assembly
will not work.
Assembly assemby = AppDomain.CurrentDomain.Load(stream.ToArray());
var type = assemby.GetExportedTypes().FirstOrDefault();
var methodInfo = type.GetMethod("Run");
var instance = Activator.CreateInstance(type);
return (string) methodInfo.Invoke(instance, new object[] {"my UserName", 12});
At this stage, we compiled and executed the C # code directly in the browser. A program can consist of several files and use other .NET libraries. Is not that great? Now we go further.
Compiling and running the Blazor component in a browser.
Blazor components are modified Razor
templates. Therefore, to compile the Blazor component, you need to deploy a whole environment for compiling Razor templates and set up extensions for Blazor. You need to install the package Microsoft.AspNetCore.Blazor.Build
from nuget. However, adding it to our Blazor project will not work, since then the linker will not be able to compile the project. Therefore, you need to download it, and then manually add 3 libraries.
microsoft.aspnetcore.blazor.build\0.7.0\tools\Microsoft.AspNetCore.Blazor.Razor.Extensions.dll
microsoft.aspnetcore.blazor.build\0.7.0\tools\Microsoft.AspNetCore.Razor.Language.dll
microsoft.aspnetcore.blazor.build\0.7.0\tools\Microsoft.CodeAnalysis.Razor.dll
Create a kernel for compilation Razor
and modify it for Blazor, since by default the kernel will generate Razor code for the pages.
var engine = RazorProjectEngine.Create(BlazorExtensionInitializer.DefaultConfiguration, fileSystem, b =>
{
BlazorExtensionInitializer.Register(b);
});
All you need to do is just fileSystem
an abstraction over the file system. We have implemented an empty file system, however, if you want to compile complex projects with support _ViewImports.cshtml
, then you need to implement a more complex structure in memory.
Now we will generate the code from the Blazor component, the C # code.
var file = new MemoryRazorProjectItem(code);
var doc = engine.Process(file).GetCSharpDocument();
var csCode = doc.GeneratedCode;
You doc
can also receive diagnostic messages about the results of generating C # code from the Blazor component.
Now we got the code for the C # component. You need to parse SyntaxTree
, then compile Assembly, load it into the current AppDomain and find the type of the component. Same as in the previous example.
It remains to load this component into the current application. There are several ways to do this, for example, by creating your own RenderFragment
.
@inject CompileService service
<divclass="card"><divclass="card-body">
@Result
</div></div>
@functions
{
RenderFragment Result = null;
string Code { get; set; }
public async Task Run()
{
var type = await service.CompileBlazor(Code);
if (type != null)
{
Result = builder =>
{
builder.OpenComponent(0, type);
builder.CloseComponent();
};
}
else
{
Result = null;
}
}
}
Conclusion
We compiled and launched the component in the browser Blazor. Obviously, a full compilation of dynamic C # code right inside the browser can impress any programmer.
But here it is necessary to take into account such "pitfalls":
- To support bidirectional binding
bind
, additional extensions and libraries are needed. - For support
async, await
, similarly connect ext. libraries - Compiling Blazor-related components will require two-step compilation.
All these problems have already been solved and this is a topic for a separate article.