Skip to content

Instantly share code, notes, and snippets.

@F-Unction
Forked from TheSnowfield/SandBox.cs
Created February 3, 2022 10:38
Show Gist options
  • Save F-Unction/ae735d61b0e9d9fcb641fd7d86cb36a5 to your computer and use it in GitHub Desktop.
Save F-Unction/ae735d61b0e9d9fcb641fd7d86cb36a5 to your computer and use it in GitHub Desktop.
Dynamically run C# code in the same context

Why we cannot use ExpandoObject

C# is not the thing what we thought about like the JavaScript, JavaScript has a global context object for the script running.
But in c# is not so easy (at least the runtime doesn't offer us that function directly).

But, how does csi.exe did it?

csi.exe initializes a CommandLineRunner for command interactive while starting. Then create a global context ScriptState<object> to save the execution results(return value, variables, methods). Once you commit the code to the terminal, it appends your code to the global context and compiles the code, and runs again or shows you the error.

But, some private and internal methods or classes prevent you to do like csi.exe do (ScriptBuilder, CSharpScriptCompiler, Script.CreateInitialScript).

About the Black Magic

It only does two things:

  • Initialize the environment for the first time you create the sandbox.
  • Grab the internal compiler from runtime.
static class BlackInteractiveMagic
{
private static readonly Type CompilerType =
Type.GetType("Microsoft.CodeAnalysis.CSharp.Scripting.CSharpScriptCompiler, " +
"Microsoft.CodeAnalysis.CSharp.Scripting");
private static readonly Type ScriptBuilderType =
Type.GetType("Microsoft.CodeAnalysis.Scripting.ScriptBuilder, " +
"Microsoft.CodeAnalysis.Scripting");
public static object Compiler
=> CompilerType!.GetField("Instance")!.GetValue(null);
public static Script<TType> CreateInitialScript<TType>(object compiler, SourceText sourceText,
ScriptOptions optionsOpt, Type globalsTypeOpt, InteractiveAssemblyLoader assemblyLoaderOpt)
{
// Create the script builder
var scriptBuilder = ScriptBuilderType.GetConstructor(new[] {typeof(InteractiveAssemblyLoader)})
!.Invoke(new[] {assemblyLoaderOpt ?? new InteractiveAssemblyLoader(null)});
// Create script instance
var scriptType = typeof(Script<>).MakeGenericType(typeof(TType));
var scriptConstr = scriptType.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic)[0];
var scriptInstance = scriptConstr.Invoke(new[]
{
compiler,
scriptBuilder,
sourceText,
optionsOpt ?? ScriptOptions.Default,
assemblyLoaderOpt, null
});
return (Script<TType>) scriptInstance;
}
}
public class SandBox
{
private readonly object _csharpCompiler;
private ScriptState<object> _globalState;
public SandBox()
{
// Create compiler
_csharpCompiler = BlackInteractiveMagic.Compiler;
// Initialize script context
var script = BlackInteractiveMagic.CreateInitialScript<object>(_csharpCompiler,
SourceText.From(string.Empty), null, null, null);
{
// Create an empty state
_globalState = script.RunAsync
(null, _ => true, default).GetAwaiter().GetResult();
}
}
public async Task<object> RunAsync(string code, CancellationToken token = default)
{
// Append the code to the last session
var newScript = _globalState.Script
.ContinueWith(code, ScriptOptions.Default);
// Diagnostics
var diagnostics = newScript.Compile(token);
foreach (var item in diagnostics)
{
if (item.Severity > DiagnosticSeverity.Error) return null;
}
// Execute the code
_globalState = await newScript
.RunFromAsync(_globalState, _ => true, token);
return _globalState.ReturnValue;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment