Skip to content

Advanced hosting scenarios

Oleg Shilo edited this page Jan 18, 2020 · 4 revisions

There were quite a few enquirers from CS-Script.Core users about implementing scripts that interact with other scripts while hosted by the same process.

This scenario is quite common when using CS-Script for .NET not .NET Core. Thus understandable many users expect the same convenience to be available on .NET Core runtime.

Problem

Unfortunately it is more complicated under .NET Core. The reason for this is that Roslyn (the only C# compiler available for .NET Core and the very compiler CS-Script.Core is using for scripting) has some fundamental flaws. Sadly Roslyn team has made a few conceptual mistakes when designing hosting model for Roslyn compiling services:

  • Problem: A user supplied C# code that is compiled and executed at runtime does not support namespace declarations.
    Impact: Users cannot use namespaces to distinguish between identically named classes.

  • Problem: A user supplied C# code that is always unconditionally wrapped in the external parent class. Meaning that any class in a script is a nested class.
    Impact: Users have no full control over the naming their classes. Thus any C# routine that uses any type (e.g. class) implemented in a script must know and use the full name of the type, which is non-deterministic. Thus by default, if you define the class Printer in the script it's name after the compilation will become Submission#0+Printer. Submission#0 parent class is injected by Roslyn class.

  • Problem: The default name of the parent class for your script uses illegal characters (Submission#0) that cannot be present in any C# code.
    Impact: None of the classes implemented in the script cannot be used as is in any C# code.

  • Problem: A script compiled with Roslyn is not loaded from a file but a memory.
    Impact: The problem seems to be very superficial at the first side. After all CLR allows dynamic assembly loading from the memory. However the problem is a complete show stopper as Roslyn refuses compiling any script that references file-less assemblies. This is a complete mind bending architectural marvel! Roslyn refuses producing assembly that have a corresponding assembly file and yet demand that all assemblies that it references must have the assembly file available. This makes it unconditionally impossible to reference one script from another.

All these awkward decisions made an impression that Roslyn team deliberately wanted to prevent users from using scripting for anything even remotely complicated and limit scripting to primitive routines like var x = 1 + 2;. Basically the team has failed to recognise the potential of their own product (e.g. implementing plugin-based architecture) and only saw the potential of scripting for REPL-like applications.

The solution

CS-Script provides a workaround for all this limitations. In fact in many cases it became possible because the required (for a workaround) functionality was already implemented in Roslyn but simply not exposed to the user in the dev-friendly way.

Thus in order to have file-full script assembly CS-Script prevents Roslyn from loading any assemblies it produces and instead it serialized them to the file and then loads from the file. Though in contrast with Roslyn Cs-Script does not force user to use one or another way of script compilation and you can use the default file-less assembly compilation (consistent with Roslyn) or you can switch to file-full compilation (available only with CS-Script):

// file-less script assembly
Assembly script = CSScript.RoslynEvaluator.CompileCode(code_text);

// file-full script assembly
var info = new CompileInfo { AssemblyFile = "<your desired location>" };
Assembly script = CSScript.RoslynEvaluator.CompileCode(code_text, info);

Problem with illegal characters in the root class name is handled in a very similar manner:

var info = new CompileInfo { RootClass = "<your desired class name>" };
Assembly script = CSScript.RoslynEvaluator.CompileCode(code_text, info);

The problem with namespaces not being supported cannot be fully solved (true Roslyn limitation) but it's impact can be addressed by the latest features of C#: using static <class name>.

Thus the solution for referencing types of one script from another one cande bone like this:

var info = new CompileInfo { RootClass = "printer_script" };

var printer_asm = CSScript.Evaluator
                          .CompileCode(@"using System;
                                         public class Printer
                                         {
                                             public void Print() => Console.Write(""Printing..."");
                                         }", info);

dynamic script = CSScript.Evaluator
                         .ReferenceAssembly(printer_asm)
                         .LoadMethod($"using static printer_script;
                                       void Test()
                                       {
                                           new Printer().Print();
                                       }");
script.Test();

Limitations

While CS-Script made it possible to use script assemblies almost the same way as normal assemblies it addresses but does not eliminate Roslyn problems (e.f. namespaces are still not supported).

Another point is that often referencing scripts in other scripts represents an anti-pattern as from decoupling point of view it is easier to share the common type definitions/interfaces (typically part of the host) and then inject the actual implementations. This way the solution is arguably is more elegant and maintainable:

public interface IPrinter
{
    void Print();
}

IPrinter printer = CSScript.Evaluator
                           .ReferenceAssemblyOf<IPrinter>()
                           .LoadCode<IPrinter>(@"using System;
                                                 public class Printer : IPrinter
                                                 {
                                                    public void Print()
                                                        => Console.Write(""Printing..."");
                                                 }");

dynamic script = CSScript.Evaluator
                         .ReferenceAssemblyOf<IPrinter>()
                         .LoadMethod(@"void Test(IPrinter printer)
                                       {
                                           printer.Print();
                                       }");
script.Test(printer);
Clone this wiki locally