SteveSandersonMS / DotNetIsolator

A library for running isolated .NET runtimes inside .NET
MIT License
604 stars 40 forks source link

Simple example of taking dynamic code for Blazor Server app #3

Closed iphdav closed 1 year ago

iphdav commented 1 year ago

Steve, this is so exciting, thank you so much!

I don't think Steve has released the Blazor spreadsheet demo yet, but I had a play myself, and if anyone wants to quickly try this, just start with the default Blazor Server template (.NET 7) then make these changes:

Add these nuget packages in csproj:

  <ItemGroup>
    <PackageReference Include="DotNetIsolator" Version="0.1.0-preview.10024" />
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.6.0-1.final" />
  </ItemGroup>

In Program.cs register the following service:

builder.Services.AddSingleton<CompilerService>();

Here is the code for the above CompilerService service using Roslyn:

using DotNetIsolator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Text;

public class CompilerService
{
    static CSharpCompilationOptions options = new(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: Microsoft.CodeAnalysis.OptimizationLevel.Release);

    static List<PortableExecutableReference> references = AppDomain.CurrentDomain.GetAssemblies()
      .Where(asm => !asm.IsDynamic && !string.IsNullOrEmpty(asm.Location))
      .Select(asm => MetadataReference.CreateFromFile(asm.Location))
      //.Concat(new[] {MetadataReference.CreateFromFile(Assembly.Load(new AssemblyName("Microsoft.CSharp")).Location) }) // Dynamic support
      .ToList();

    // We will put generated assemblies here
    static Dictionary<string, byte[]> assemblies = new();

    static IsolatedRuntimeHost host = new IsolatedRuntimeHost()
    .WithBinDirectoryAssemblyLoader()
    .WithAssemblyLoader(name => assemblies.ContainsKey(name) ? assemblies[name] : null);

    public string CompileAndRun(string code)
    {
        var parsedCode = CSharpSyntaxTree.ParseText(SourceText.From($$"""
            using System;
            using System.IO;
            using System.Linq;
            using System.Text.Json;
            using System.Collections.Generic;
            using static System.Console;
            using static System.Math;
            using static System.Text.Json.JsonSerializer;

            public class MyLib
            {
                public static string RunCode()
                {
                    var writer = new StringWriter();
                    Console.SetOut(writer);
                    Console.SetError(writer);
                    {{code}}
                    Console.Out.Flush();
                    return writer.ToString();
                }
            }
            """));

        var assemblyName = Guid.NewGuid().ToString();
        using var templateAssemblyStream = new MemoryStream();
        using var templatePdbStream = new MemoryStream();

        var compilation = CSharpCompilation.Create(assemblyName, new SyntaxTree[] { parsedCode }, references, options);
        var compilationResult = compilation.Emit(templateAssemblyStream, templatePdbStream, options: new(debugInformationFormat: DebugInformationFormat.Pdb));
        if (!compilationResult.Success)
        {
            return string.Join("\n", compilationResult.Diagnostics.Select(i => i.ToString()));
        }
        else
        {
            assemblies.Add(assemblyName, templateAssemblyStream.ToArray());
        }

        var runtime = new IsolatedRuntime(host);

        // Now we will invoke the method
        var method = runtime.GetMethod(assemblyName, null, null, "MyLib", "RunCode");
        return method.Invoke<string>(null);
    }
}

Then in the Blazor Server app's Index.razor file, replace content with this code:

@page "/"
@inject CompilerService cs

<h1>Compiler Service</h1>

<textarea @bind="source" @bind:after="Compile" @bind:event="oninput" style="width:100%" rows="10"></textarea> 

<pre style="white-space:pre-wrap">
    @output
</pre>

@code {
    string source = "// var num = 10; Console.WriteLine(\"Number was: \" + num);";
    string output = "Type some code to compile and run.";

    void Compile()
    {
        try
        {
            output = cs.CompileAndRun(source);
        }
        catch (Exception ex)
        {
            output = ex.Message;
        }
    }
}

Now you can type code like this:

var name = "David";
Console.WriteLine("Hello " + name);

It recompiles with every keystroke. Note I am currently caching the old assemblies generated in a list, so this is just a demo, but lots of fun!

SteveSandersonMS commented 1 year ago

Cool, thanks for posting this example!

I'll mark the issue as closed since it's not tracking any pending work, but you can continue to link people here and people are welcome to continue discussing this below.