Search code examples
c#cs-script

CS-Script Evaluator LoadCode: How to compile and reference a second script (reusable library)


The question in short is: How do you reference a second script containing reusable script code, under the constraints that you need to be able to unload and reload the scripts when either of them changes without restarting the host application?

I'm trying to compile a script class using the CS-Script "compiler as service" (CSScript.Evaluator), while referencing an assembly that has just been compiled from a second "library" script. The purpose is that the library script should contain code that can be reused for different scripts.

Here is a sample code that illustrates the idea but also causes a CompilerException at runtime.

using CSScriptLibrary;
using NUnit.Framework;

[TestFixture]
public class ScriptReferencingTests
{
    private const string LibraryScriptCode = @"
public class Helper
{
    public static int AddOne(int x)
    {
        return x + 1;
    }
}
";

    private const string ScriptCode = @"
using System;
public class Script
{
    public int SumAndAddOne(int a, int b)
    {
        return Helper.AddOne(a+b);
    }
}
";

    [Test]
    public void CSScriptEvaluator_CanReferenceCompiledAssembly()
    {
        var libraryEvaluator = CSScript.Evaluator.CompileCode(LibraryScriptCode);
        var libraryAssembly = libraryEvaluator.GetCompiledAssembly();
        var evaluatorWithReference = CSScript.Evaluator.ReferenceAssembly(libraryAssembly);
        dynamic scriptInstance = evaluatorWithReference.LoadCode(ScriptCode);

        var result = scriptInstance.SumAndAddOne(1, 2);

        Assert.That(result, Is.EqualTo(4));
    }
}

To run the code you need NuGet packages NUnit and cs-script.

This line causes a CompilerException at runtime:

dynamic scriptInstance = evaluatorWithReference.LoadCode(ScriptCode);

{interactive}(7,23): error CS0584: Internal compiler error: The invoked member is not supported in a dynamic assembly.

{interactive}(7,9): error CS0029: Cannot implicitly convert type '<fake$type>' to 'int'

Again, the reason for using CSScript.Evaluator.LoadCode instead of CSScript.LoadCode is so that the script can be reloaded at any time without restarting the host application when either of the scripts changes. (CSScript.LoadCode already supports including other scripts according to http://www.csscript.net/help/Importing_scripts.html)

Here is the documentation on the CS-Script Evaluator: http://www.csscript.net/help/evaluator.html

The lack of google results for this is discouraging, but I hope I'm missing something simple. Any help would be greatly appreciated.

(This question should be filed under the tag cs-script which does not exist.)


Solution

  • The quick solution to the CompilerException appears to be not use Evaluator to compile the assembly, but instead just CSScript.LoadCode like so

    var compiledAssemblyName = CSScript.CompileCode(LibraryScriptCode);
    var evaluatorWithReference = CSScript.Evaluator.ReferenceAssembly(compiledAssemblyName);
    dynamic scriptInstance = evaluatorWithReference.LoadCode(ScriptCode);
    

    However, as stated in previous answer, this limits the possibilities for dependency control that the CodeDOM model offers (like css_include). Also, any change to the LibraryScriptCode are not seen which again limits the usefulness of the Evaluator method.

    The solution I chose is the AsmHelper.CreateObject and AsmHelper.AlignToInterface<T> methods. This lets you use the regular css_include in your scripts, while at the same time allowing you at any time to reload the scripts by disposing the AsmHelper and starting over. My solution looks something like this:

    AsmHelper asmHelper = new AsmHelper(CSScript.Compile(filePath), null, false);
    object obj = asmHelper.CreateObject("*");
    IMyInterface instance = asmHelper.TryAlignToInterface<IMyInterface>(obj);
    // Any other interfaces you want to instantiate...
    ...
    if (instance != null)
        instance.MyScriptMethod();
    

    Once a change is detected (I use FileSystemWatcher), you just call asmHelper.Dispose and run the above code again.

    This method requires the script class to be marked with the Serializable attribute, or simply inherit from MarshalByRefObject.
    Note that your script class does not need to inherit any interface. The AlignToInterface works both with and without it. You could use dynamic here, but I prefer having a strongly typed interface to avoid errors down the line.

    I couldn't get the built in try-methods to work, so I made this extension method for less clutter when it is not known whether or not the interface is implemented:

    public static class InterfaceExtensions
    {
        public static T TryAlignToInterface<T>(this AsmHelper helper, object obj) where T : class
        {
            try
            {
                return helper.AlignToInterface<T>(obj);
            }
            catch
            {
                return null;
            }
        }
    }
    

    Most of this is explained in the hosting guidelines http://www.csscript.net/help/script_hosting_guideline_.html, and there are helpful samples mentioned in previous post.

    I feel I might have missed something regarding script change detection, but this method works solidly.