Home .NET Scripting in C# or dynamic runtime execution

Scripting in C# or dynamic runtime execution

by admin

Hello Hubr!

I don’t think many people know that C# has a thing like eval from other languages. Thanks to Roslyn API, you can compile and execute C# code at runtime. You can see an example of how to use it at my implementation REPL -a for C#.

The first time I was introduced to such a thing as REPL was when I was touching python. In the .NET world, there is a similar thing called C# Interactive (CSI). Pretty handy thing, but it has one big minus – it is a part of Visual Studio tools, so you can’t use it without VS installation, and to run it without VS startup you have to go into its bowels (more precisely, to run C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\Tools\VsDevCmd.bat) through console, which might not be a very convenient solution.

There are also projects such as dotnet-script and cs-script (they work through Microsoft.CodeAnalysis.CSharp.Scripting ), but they have a fatal flaw – they’re not written by me. That’s why I had an idea to write my own crummy bicycle with my own features (that also work crudely)! After a short search, my eyes fell on this miracle: Microsoft.CodeAnalysis.CSharp.Scripting Among the pluses – handy API, ability to execute code without classes and namespaces.

First of all you need to put the nugget package Microsoft.CodeAnalysis.CSharp.Scripting and make using

using Microsoft.CodeAnalysis.CSharp.Scripting;using Microsoft.CodeAnalysis.Scripting;

CSharpScript is a static class that helps us create a script, includes 3 methods :

  • Create – creates Script with specified code and parameters, which can be later compiled and run

  • RunAsync – which compiles, executes the passed code and returns ScriptState

  • EvaluateAsync – executes code and returns the result of execution

CSharpScript.Create can be used when you need to pre-compile a script and call it often.

var script = CSharpScript.Create("System.Console.WriteLine(\"Hello from script\")");script.Compile();Await script.RunAsync();

If you don’t call Compile(), the code will be compiled on the first call.

For convenience, you can create ScriptOptions, where you can add namespaces and references (you can also add static classes, like using static).

var options = ScriptOptions.Default.AddImports("System", "System.IO", "System.Collections.Generic", "System.Console", "System.Diagnostics", "System.Dynamic", "System.Linq", "System.Text", "System.Threading.Tasks").AddReferences("System", "System.Core", "Microsoft.CSharp");CSharpScript.Create("Console.WriteLine(\"Hello from script\")", options);

But here’s the thing: ScriptOptions doesn’t limit available namespaces for some reason. Kind of a whitelist I originally thought, maybe I just didn’t fully understand it.

CSharpScript.RunAsync returns a ScriptState, you can augment it by calling ContinueWithAsync, which will compile, execute the code and return a new ScriptState object. You can re-run the script by accessing the Script property. To get the result, there is the ReturnValue property.

ScriptState state = await CSharpScript.RunAsync("int x = 5;");state = await state.ContinueWithAsync<int> ("x + 1");Console.WriteLine(state.ReturnValue); // 6

You can see the declared variables of the state object, as well as the resulting exception

foreach(var variable in state.Variables){Console.WriteLine($"{variable.Name}- {variable.Value}");}

With CSharpScript.Create, you can create a delegate from the script that will run the script when you call

var script = CSharpScript.Create<Func<int, int> > ("x => x+1");Console.WriteLine(await script.CreateDelegate().Invoke(1)); // 2

Alternatively, you can compile a lambda expression as a string using CSharpScript.EvaluateAsync (or the other methods above)

var deleg = await CSharpScript.EvaluateAsync<Func<int, int> > ("x => x * 2");Console.WriteLine(deleg(5)); // 10

This could be useful for serializing and deserializing lambda expressions (my feeble mind couldn’t think of a usecase for this, but I’ve met people who needed such a thing).

CSharpScript methods have globals and globalsType parameters (globalsType can be left out, it will take the type from globals), which can be used to specify an object whose members will be globally accessible (CSharpScript.Create can only specify globalsType, and pass globals in script.RunAsync()).

var res = await CSharpScript.EvaluateAsync<int> ("X+Y", globals: new GlobalValues());Console.WriteLine(res); // 100public class GlobalValues{public int X = 25;public int Y = 75;}

Below are the tests :

Scripting in C# or dynamic runtime execution

More examples can be found in the links below :

https://github.com/dotnet/roslyn/blob/main/docs/wiki/Scripting-API-Samples.md

https://github.com/dotnet/roslyn/tree/a7319e2bc8cac34c34527031e6204d383d29d4ab/src/Scripting

I hope my first article didn’t seem too boring, and that I was able to help you in some way.

Have a great day!

You may also like