C# Scripting (in the .NET Core world)

Filip W

not PM. not architect. not developer advocate.

strathweb.com@filip_wojgithub.com/filipw

Run a script with script runner

    
C:\>csi script.csx scriptArgs

C:\>scriptcs script.csx -- scriptArgs

C:\>dotnet script script.csx -- scriptArgs

C:\>cake build.cake -Target build
    

Use Roslyn APIs

    
var scriptCode = File.ReadAllText("script.csx");
var scriptOptions = ScriptOptions.Default.
    WithImports("System.Net.Http").
    WithReferences(typeof(HttpClient).GetTypeInfo().Assembly);

// Run
var scriptResult = await CSharpScript.RunAsync(scriptCode, scriptOptions);

// Create and run later
var script = await CSharpScript.Create(scriptCode, scriptOptions);
var diagnostics = script.GetCompilation().GetDiagnostics();
var scriptResult = await script.RunAsync();
    

Scripting Key Features

  • 🎉 C# scripting is cross platform
    Mono 4.6+ support, .NET Core
  • 💯 CSI is on your machine already
    Bundled with MSBuild Tools
  • 💻 Script & Interactive Mode
    .csx, C# REPL
  • 🔬 Works with any .NET assemblies
    Can marshal objects between host app and script

1st class Roslyn citizen

  • 📐 Syntax
    SourceCodeKind.Script vs SourceCodeKind.Regular
  • 📚 Language services
    ProjectInfo knows about host object Type & global usings
  • 🖥 Script Compilation
    Shares logic with standard CSharpCompilation
  • 📦 Development
    Microsoft.CodeAnalysis.Csharp.Scripting

.NET Core Scripting

   

.NET Core support Standard syntax
Roslyn libraries 🦄 N/A
CSI 💩
scriptcs 💩
Cake 🔧
dotnet-script 🦄

Use cases

Popular C# Scripting Use Cases

  • ⚙️ Automation
    Batch jobs, build scripts, command line tools
  • 🍃 Lightweight programs
    Experiments, API exploration
  • 🖇 Application extensibility
    Plugins, modules, dynamic configuration
  • ⚗️ Code generation
    T4-like features, emitting code

Emit code with IL

    
var name = new AssemblyName("DynamicAssembly");
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicModule");
var typeBuilder = moduleBuilder.DefineType("DynamicClass", TypeAttributes.Public);
var methodBuilder = typeBuilder.DefineMethod("DynamicMethod", MethodAttributes.Public, typeof(int), new[] { typeof(int), typeof(int) });

var ilGenerator = methodBuilder.GetILGenerator();

ilGenerator.DeclareLocal(typeof(int));
ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Ldarg_2);
ilGenerator.Emit(OpCodes.Sub_Ovf);
ilGenerator.Emit(OpCodes.Stloc_0);
ilGenerator.Emit(OpCodes.Ldloc_0);
ilGenerator.Emit(OpCodes.Ret);

var createdType = typeBuilder.CreateTypeInfo().AsType();

var instance = Activator.CreateInstance(createdType);
var methodInfo = createdType.GetMethod("DynamicMethod", BindingFlags.Instance | BindingFlags.Public);

var result = methodInfo.Invoke(instance, new object[] { 5, 3 });
    

Emit code with Roslyn compilation

    
var compilation = CSharpCompilation.Create(
    "DynamicAssembly", new[] { CSharpSyntaxTree.ParseText(@"
     public class DynamicClass {
        public int DynamicMethod(int a, int b) {
            return a-b;
        }
     }") }, new[] { MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location) },
    new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

using (var ms = new MemoryStream())
{
    var cr = compilation.Emit(ms);
    ms.Seek(0, SeekOrigin.Begin);
    var assembly = AssemblyLoadContext.Default.LoadFromStream(ms);

    var createdType = assembly.ExportedTypes.FirstOrDefault(x => x.Name == "DynamicClass");
    var methodInfo = createdType.GetMethod("DynamicMethod", BindingFlags.Instance | BindingFlags.Public);
    var instance = Activator.CreateInstance(createdType);

    var result = methodInfo.Invoke(instance, new object[] { 5, 3 });
}
    

Emit code with Roslyn scripting

    
var result = await CSharpScript.EvaluateAsync<int>("5 - 3");

// or
var script = CSharpScript.Create<int>("5 - 3");
var handle = script.CreateDelegate();
await handle();

// or
public class ScriptHost
{
    public int Number1 { get; set; }
    public int Number2 { get; set; }
}

var host = new ScriptHost { Number1 = 5, Number2 = 3 };
var result = await CSharpScript.EvaluateAsync<int>("Number1 - Number2", globals: host, globalsType: typeof(ScriptHost));
    

Editor Features

CSX as a valid launch target

Realtime #r & #load

Go to Metadata

Refactoring support

Language features at "regular" C# level

Symbols

Nuget

Inline Nuget Packages in CSX

    
#r "nuget:AutoMapper, 5.1.1"

using AutoMapper;
Console.WriteLine(typeof(MapperConfiguration));
    

How does it all work?

Consider a simple script...

    
class Foo {}

void SayHi(string msg)
{
    Console.WriteLine(msg);
}

var message = "Hello";
var foo = new Foo();

SayHi(message + " " + Text);
    

    
public sealed class Submission#0 {
    public string message;
    public string Submission#0.Foo foo;
    public void SayHi(string msg) {
        Console.WriteLine(msg);
    }

    public class Foo {};

    public MyHost <host-object>;

    // shortened or brevity
    public sealed class <<Initialize>>d__0 : IAsyncStateMachine {
        public Submission#0 <>4__this;

        void.IAsyncStateMachine MoveNext() {
            this.<>4__this.message = "hello!";
            this.<>4__this.foo = new Submission#0.Foo();

            this.<>4__this.SayHi(this.<>4__this.message + " " + this.<>4__this.<host-object>.Text);
        }
    }

    public Submission#0(object[] submissionArray) {
        submissionArray[1] = this;
        this.<host-object> = (MyHost)submissionArray[0];
    }

    public static Task<object> <Factory>(object[] submissionArray) {
        return new Submission#0(submissionArray).<Initialize>();
    }

}