In this article, we're going to talk about using C# Reflection functionality from a scripting language. The main idea is that very well-known and debugged parts of C# code can be reused in scripting. A use case for using Reflection and a scripting language might be a scenario where there's already a client class library that does something useful (in C#) and you want to additionally control it from the scripting code to get more flexibility.

As an example, we're going to use the PCmover service developed by Laplink Software. PCmover helps you to upgrade an old PC to a new one by transferring all of your files and installed programs to your new computer so that you're up and running on your new computer very quickly and easily.

Why would you want to control these operations from the scripting code? With scripting, you can use the technology in new ways that go beyond what the basic user interface allows. This can include special customization or selection of the computers that are involved, among other things.

One customer may want to transfer from one computer to several computers. The service supports that but the GUI doesn't. That customer could write scripts to do that. Another customer may want to transfer only a certain number of files from one part of a file system. Another customer may want to add extra security checks on certain types of transfers. There are many things that could be done but that the GUI isn't well suited to do. Scripting opens up all the capabilities to the customers without having to create custom versions of the GUI for them.

The goal of this article is to show how you can easily take C# code from a complex service and create CSCS code that looks very similar to the C# code, so that the scripting language now has access to a very complicated existing DLL.

As a scripting language, we're going to use CSCS (Customized Scripting in C#). This is an easy and lightweight open-source language that has been described in previous CODE Magazine articles: https://www.codemag.com/article/1607081 introduced it, https://www.codemag.com/article/1711081 showed how you can use it on top of Xamarin to create cross-platform native mobile apps, and https://www.codemag.com/article/1903081 showed how you can use it for Unity programming. CSCS resembles JavaScript with a few differences, e.g., the variable and function names are case insensitive.

Using reflection, very little code is needed to access existing .NET code via scripting. In this article, we'll show you how to do it and how it's implemented.

Let's start by looking at how reflection can be incorporated into scripting.

A Brief Introduction to the PCmover Application

The easiest way to include the CSCS scripting into your application is to download the CSCS project from the GitHub and include it in your work space. You should also include a reference to the CSCS project in your project. See Figure 1 for details.

Figure 1: A sample PCmover application Visual Studio project
Figure 1: A sample PCmover application Visual Studio project

Here's the description of the various modules that are part of the workspace in Figure 1:

  • PcmoverComponent: This is presumed to be an existing DLL that implements a PCmover Service. It's very complicated (not in this example, but it is in real life), doing a large amount of work to transfer all of your files and programs from your old computer to the new computer. By using scripting with reflection, you can easily take C# code that uses this service and create CSCS code that looks very similar to the C# code, so that the scripting language now has access to this very complicated existing DLL.
  • ArticleConsole: This is the top-level console app that you build and run. Before launching the script, it runs a function called TestPcmoverComponent, which calls a series of functions in PcmoverComponent, demonstrating how it would be used with pure C# code.
  • PcmoverCscsModule: This is the PcmoverModule component that links the PcmoverComponent code into CSCS. It implements only one function: GetPcmover, which returns a PcmoverService object. Everything else in CSCS is done via reflection through that object.
  • CSCS: Core CSCS code, referenced in the solution file. Implements core CSCS parsing.
  • CSCS.ConsoleApp: CSCS.ConsoleApp code, referenced in the solution file. Implements the console functionality around CSCS scripting.
  • Modules used by CSCS.
    • CSCS.InterpreterManager: Implements multiple interpreters in CSCS
    • CSCS.Math: Implements CSCS math functions
    • CSCS.SqlClient: Implements CSCS client connectivity to a SQL database.

You can add an arbitrary number of custom modules. You do it in C# in the initialization stage. Note that each module is compiled into a DLL and it doesn't have to be added in C# code or even be a part of your workspace. In this case, you can add an arbitrary module at runtime as follows:

Import ("CSCS.Math");

When doing such an import, the CSCS interpreter is looking for files with the name CSCS.Math.dll.

“Hello, World!” in Reflection with Scripting

Let's start with a very simple class like this one:

namespace HelloCscsModule 
{
  public class HelloWorldService
  {
    public string Hello(string name) {
      return "Hello, " + name + "!";
    }
  }
}

The idea is to use methods declared in this class without also declaring them in a scripting module. In this case, there's just one method, Hello().

To be able to use this HelloWorldService class, you need to define a class deriving from ParserFunction and override its Evaluate() method. This functionality is the same for any other CSCS scripting function. See https://www.codemag.com/Article/2209051 for some examples.

Here's an implementation of such a class, returning a variable that is a wrapper over the HelloWorldService object:

internal class GetHelloWorldFunction: ParserFunction
{
  protected override Variable Evaluate(ParsingScript script)
  {
    return new Variable(new HelloWorldService());
  }
}

The last step is to register the GetHelloWorldFunction from the previous step with the interpreter. This is done by executing the following statement in the initialization phase:

interpreter.RegisterFunction("GetHello",
   new HelloCscsModule.GetHelloWorldFunction());

That's it! As soon as the interpreter extracts the GetHello token, it returns a handle to the underlying HelloWorldService object. Now you can execute any method defined for the HelloWorldService class in the C# code. In this case, it's just the Hello() method. It's triggered by interpreter using reflection.

Here's an example in CSCS:

hi = GetHello();
hi.Hello("world");

Here's the output after executing the two statements above:

HelloCscsModule.HelloWorldService
Hello, world!

In the same manner, you can invoke any method or a property defined in C#.

In the next section, you're going to see a more real-life example with the PCmover service from Laplink.

Example of PCmover Scripting

Let's look at real C# code that interacts with the PCmover service and see how easy it is to convert that to script code. This is the TestPcmoverComponent function in C# that's used to run a transfer.

First, initialize required components:

void TestPcmoverComponent() {
  var pcmoverService = new PcmoverService();
  Print("Testing PCmover component in C#");

  var thisMachine = pcmoverService.ThisMachine;
  Print("This machine is named " +
        thisMachine.Name + ", version " +
        thisMachine.WindowsVersion);

  var otherMachine = pcmoverService.GetMachine("SlowOld");
  if (otherMachine == null) {
      Print("Unable to get the other machine");
      return;
  }
  Print("The other machine is named " +
        otherMachine.Name + ", version " +
        otherMachine.WindowsVersion);

Then create a Transfer object and mimic a transfer:

  Transfer transfer = pcmoverService.
    CreateTransfer(otherMachine, thisMachine);
  if (transfer.Status != TransferStatus.Ready) {
    Print("Transfer is not Ready");
    return;
  }
  if (!transfer.Start()) {
    Print("Error starting transfer");
    return;
  }
  while (transfer.Status == TransferStatus.Active) {
    Print("Waiting for transfer to complete...");
    Thread.Sleep(1000);
  }
  Print("Transfer complete!");
}

This code sets up a transfer from the old computer, named SlowOld, to the new computer. It displays some details along the way and some progress while it waits for the transfer to complete. The output of this C# code looks like this:

Testing PCmover component in C#
This machine is named ShinyNew, version 11.0
The other machine is named SlowOld, version 10.0
Waiting for transfer to complete...
Waiting for transfer to complete...
Waiting for transfer to complete...
Waiting for transfer to complete...
Waiting for transfer to complete...
Transfer complete!

Now you want to write the same functionality in script. Just like with the simple Hello World example, all you need to implement in the scripting environment is a function to get the initial PcmoverService object. Everything else works automatically, with a few small tweaks, via reflection. Here's the CSCS code that does the same thing:

function TestPcmoverComponent() {
  pcmoverService = GetPcmover();
  Print("Testing PCmover component in CSCS");

  thisMachine = pcmoverService.ThisMachine;
  Print("This machine is named " + 
        thisMachine.Name + ", version " + 
              thisMachine.WindowsVersion);
  otherMachine = pcmoverService.GetMachine("SlowOld");
  if (otherMachine == null) {
    Print("Unable to get the other machine");
    return;
  }
  Print("The other machine is named " +
        otherMachine.Name + ", version " +
        otherMachine.WindowsVersion);

Just like its C# counterpart, you create a Transfer object and do the actual transfer after the initialization stage:

  transfer = pcmoverService.CreateTransfer(
             otherMachine, thisMachine);
  if (transfer.Status != "Ready") {
    Print("Transfer is not Ready");
    return;
  }
  if (!transfer.Start()) {
    Print("Error stating transfer");
    return;
  }
  while (transfer.Status == "Active") {
    Print("Waiting for transfer to complete...");
    sleep(1000);
  }
  Print("Transfer complete!");
}

This code is almost line-for-line identical to the C# code, with just a few differences. Those differences are:

  • Syntactically, CSCS does not define functions the same way as C#.
  • Syntactically, CSCS does not declare variables with their types. You just use them.
  • The CSCS code uses the special GetPcmover function to get the pcmoverService object instead of the “new” syntax of the C# code.
  • When checking the value of transfer.Status, C# uses the enum value, but CSCS uses a string matching the enum name.
  • CSCS uses the built-in “sleep” function rather than Thread.Sleep().

This is the output of the CSCS version:

Testing PCmover component in CSCS
This machine is named ShinyNew, version 11.0
The other machine is named SlowOld, version 10.0
Waiting for transfer to complete...
Waiting for transfer to complete...
Waiting for transfer to complete...
Waiting for transfer to complete...
Waiting for transfer to complete...
Transfer complete!

That's it! With just a handful of CSCS syntactic changes and a simple function to expose an existing .NET object, you can implement full access to an existing library. Reflection handles access to all of the functionality of that object, and any other objects you retrieve through it. This example uses a PcmoverService object, but it could be a game service, or a geographical service, or any other existing library that you have that you'd like to control with scripting.

Good programmers write good code. Great programmers write no code. Zen programmers delete code. – John Byrd

Using Reflection in Scripting

In this section, you'll see code snippets of the implementation of reflection in CSCS. The full version is available on GitHub (https://github.com/vassilych/cscs).

As soon as the scripting parsing interpreter encounters a name after a dot, the following method checks whether you can use reflection to either extract the value of the property or to execute a method with that name:

Variable GetReflectedProperty(string propName, ParsingScript script) {
    BindingFlags bf = BindingFlags.Instance |
                      BindingFlags.Public;
    var property = FindNestedMatchingProperty(
        ObjectType, propName,
        bf | BindingFlags.GetProperty);
    if (property != null) {
        object val = property.GetValue(Object);
        return ConvertToVariable(val,
            property.PropertyType);
  }

First, try to find the property match. If not successful, look for methods:

  var pConv = new ParameterConverter();
  var bestMethod = pConv.FindBestMethod(
      ObjectType, propName, GetArgs(script), bf);
  if (bestMethod != null} {
      var res = bestMethod.Invoke(Object, pConv.BestTypedArgs);
      return ConvertToVariable(res, bestMethod.ReturnType);
  }
  return null;
}

FindBestMethod() scans the methods to find one that best matches the parameters that are passed from the script.

MethodInfo FindBestMethod(Type t, string 
 propName, List<Variable> args, BindingFlags bf)
{
    MethodInfo bestMethod = null;
    var methods = t.GetMethods(bf);
    if (methods != null) {
        foreach (var method in methods) {
            if (String.Compare(method.Name, propName, true) == 0) {
                var parameters = method.GetParameters();
                if (ConvertVariablesToTypedArgs(args, parameters)) {
                    bestMethod = method;
                    if(BestConversion == Conversion.Exact)
                        break;
                }
            }
        }
    }
    return bestMethod;
}

The C# implementation details of using reflection from scripting are shown in Listing 1.

Listing 1: Reflection implementation for scripting in C# (TO BEAUTIFY)

public enum Conversion
{
    Exact,
    Assignable,
    Convertible,
    Mismatch
}

public static Conversion ChangeTypes(List<Variable> args, 
    ParameterInfo[] parameters, object[] typedArgs)
{
    Conversion worstConversion = Conversion.Exact;
    if (args.Count > 0)
    {
        for (int arg = 0; arg < args.Count; ++arg)
        {
            typedArgs[arg] = ChangeType(args[arg].AsObject(), 
                parameters[arg].ParameterType, out Conversion conversion);
            if (conversion > worstConversion) worstConversion = conversion;
        }
    }
    return worstConversion;
}

public static object ChangeType(object value, Type conversionType)
{
    return ChangeType(value, conversionType, out Conversion conversion);
}

public static object ChangeType(object value, Type conversionType, 
    out Conversion conversion)
{
    try
    {
        Type underlyingType = Nullable.GetUnderlyingType(conversionType);

        if (value == null)
        {
            if (underlyingType == null && conversionType.IsValueType)
            {
                conversion = Conversion.Mismatch;
            }
            else
            {
                conversion = Conversion.Exact;
            }

            return value;
        }

        Type t = value.GetType();
        if (t == conversionType)
        {
            conversion = Conversion.Exact;
            return value;
        }

        if (t == underlyingType)
        {
            conversion = Conversion.Convertible;
            return value;
        }

        if (conversionType.IsAssignableFrom(t))
        {
            conversion = Conversion.Assignable;
            return value;
        }

        if (underlyingType != null && underlyingType.IsAssignableFrom(t))
        {
            conversion = Conversion.Convertible;
            return value;
        }

        conversion = Conversion.Convertible;
        if (conversionType.IsEnum)
        {
            if (value is string svalue)
            {
               return Enum.Parse(conversionType, svalue, true);
            }

            if (value is double dvalue)
            {
                return Enum.ToObject(conversionType, (long)dvalue);
            }

            // Let's see if it's some other type that just happens to work
            conversion = Conversion.Mismatch;
            return Enum.ToObject(conversionType, value);
        }

        IList genericList = ConvertToGenericList(value, conversionType);
        if (genericList != null)
        {
            AddToGenericList(genericList, value);
            return genericList;
        }
        try
        {
            return Convert.ChangeType(value, conversionType);
        }
        catch (Exception)
        {
            if (underlyingType != null)
            {
                return Convert.ChangeType(value, underlyingType);
            }
        }
    }
    catch { }
    conversion = Conversion.Mismatch;
    return value;
}

Experience is the name everyone gives to their mistakes. – Oscar Wilde

Using Events

The code that we've worked on so far had one undesirable aspect. While the transfer was taking place, the code was sitting in a loop polling for whether the transfer was complete, displaying a generic message each time it checked. It would be nice to get asynchronous notification of the progress of the transfer. Fortunately, this PCmover component supports a Progress event. Let's see how to incorporate that into the script client.

The PCmover component defines a ProgressInfo object, which the Transfer object passes to a Progress event, like this:

public class ProgressInfo {
  public Transfer Transfer { get; set; }
  public string Action { get; set; }
  public int Percent { get; set; }
}

public class Transfer {
  public delegate void ProgressHandler(ProgressInfo info);
  public event ProgressHandler Progress;

  void Fire Progress(string action, int percent) {
      Progress?.Invoke(new ProgressInfo(this, action, percent);
  }
      ...
}

To use it, modify the TestPcmoverComponent code to subscribe to the event and have the event handler display the progress message and set an event when the transfer completes. The main code just waits for that event.

AutoResetEvent _transferComplete;

void OnProgress(ProgressInfo info) {
  Print(info.Action + " " + info.Percent + "%");
        if (info.Transfer.Status == 
            TransferStatus.Complete)
          _transferComplete.Set();
}

void TestPcmoverComponent() {
// Setup is the same, until the Transfer:
  _transferComplete = new AutoResetEvent(false);
  Transfer.Progress += OnProgress;
        if (!transfer.Start()) {
    Print("Error starting transfer");
    return;
  }
  _transferComplete.WaitOne();
  Print("Transfer complete!");
}

Now, when you run TestPcmoverComponent, you get progress messages:

Testing PCmover component in C#
This machine is named ShinyNew, version 11.0
The other machine is named SlowOld, version 10.0
Transferring Files 0%
Transferring Files 12%
Transferring Files 23%
Transferring Files 35%
Transferring Files 46%
Transferring Files 58%
Transferring Registry 70%
Transferring Registry 81%
Transferring Registry 93%
Complete 100%
Transfer complete!

See Listing 2 to test PCmover implementation code in scripting.

Listing 2: CSCS code to test PCmover implementation in scripting

function TestPcmoverComponent()
{
    pcmoverService = GetPcmover();

    Print("Testing PCmover component in CSCS");

    thisMachine = pcmoverService.ThisMachine;
    Print("This machine is named " + thisMachine.Name +
          ", version " + thisMachine.WindowsVersion);

    otherMachine = pcmoverService.GetMachine("SlowOld");
    if (otherMachine == null)
    {
        Print("Unable to get the other machine");
        return;
    }
    Print("The other machine is named " + 
        otherMachine.Name + ", version " + 
       otherMachine.WindowsVersion);

    transfer = pcmoverService.CreateTransfer(
               otherMachine, thisMachine);
    if (transfer.Status != "Ready")
    {
        Print("Transfer is not Ready");
        return;
    }

    if (!transfer.Start())
    {
        Print("Error starting transfer");
        return;
    }

    while (transfer.Status == "Active")
    {
        Print("Waiting for transfer to complete...");
        sleep(1000);
    }

    Print("Transfer complete!");
}
public class NewObjectFunction : ParsingFunction
{
  protected override Variable Evaluate(ParsingScript script)
  {
    string className = Utils.GetToken(script);
    className = Constants.ConvertName(className);
    List<Variable> args = script.GetFunctionArgs();

    var csClass = CSCSClass.GetClass(className) as CompiledClass;
    if (csClass != null) {
        ScriptObject obj = csClass.GetImplementation(args);
        return new Variable(obj);
    }
    var instance = new CSCSClass.ClassInstance(
      script.CurrentAssign, className, args, script);

    var newObject = new Variable(instance);
    newObject.ParamName = instance.InstanceName;
    return newObject;
  }
}

Getting Events in Script

So, how do you do the equivalent in the script code? Although script code could directly access the Transfer event object, it has no way to directly add a delegate to it. To do that, in the script support module for PCmover, you need the GetPcmover function to return a wrapper object around the PcmoverService object. This lets you extend the functionality. The extra functionality you want to add is to create a transfer object that fires events.

internal class PcmoverServiceWrapper :
               PcmoverService {
        Interpreter InterpreterInstance { get; }
        public PcmoverServiceWrapper(
            Interpreter interpreter){
            InterpreterInstance = interpreter;
  }

  public Transfer CreateProgressTransfer(
     Machine oldMachine,
     Machine newMachine,
     string onProgress) {
          var transfer = new Transfer(oldMachine, newMachine);
          transfer.Progress += (info) =>>
         {
             InterpreterInstance.Run(onProgress, new Variable(info);
         }
         return transfer;
     }
}

The script can now call CreateProgressTransfer, instead of CreateTransfer, passing the name of a script function that should execute when the Progress event fires. All that's left is to modify the script to use it:

function OnProgress(info) {
  Print(info.Action + " " + info.Percent + "%");
        if (info.Transfer.Status == "Complete")   {
          Signal();
        }
}

void TestPcmoverComponent( ) {
    // Setup is the same, until the Transfer:
    transfer = pcmoverService.CreateProgessTransfer(
       otherMachine, thisMachine, "OnProgress");
    ...
    // Then, instead of polling:
    Wait();
    Print("Transfer complete!");
}

You have to call the wrapper's CreateProgressTransfer function, which did the plumbing related to setting up the event handler. The event handler used the interpreter's Run function to execute the requested script code, passing it the same ProgressInfo object. Script used the CSCS Wait and Signal mechanism for the event handling. Otherwise, the code is practically the same as the C# code. And the output is the same as well:

Testing PCmover component in CSCS
This machine is named ShinyNew, version 11.0
The other machine is named SlowOld, version 10.0
Transferring Files 0%
Transferring Files 12%
Transferring Files 23%
Transferring Files 35%
Transferring Files 46%
Transferring Files 58%
Transferring Registry 70%
Transferring Registry 81%
Transferring Registry 93%
Complete 100%
Transfer complete!

Wrapping Up

In this article, you saw how you can use reflection in a scripting language to access an existing and good working .NET library.

The code shown in this article is just an invitation to explore. After reading it, we hope you can implement scripting via reflection in .NET libraries that you use.

We're looking forward to your feedback, specifically, your experience in applications you're accessing with reflection from scripting and also what other features in CSCS you would like to see.

Table 1: Resources

Laplink PCmover history  https://en.wikipedia.org/wiki/Laplink_PCmover
Laplink PCmover  https://go.laplink.com/
CSCS Repository https://github.com/vassilych/cscs
CSCS Visual Studio Code Debugger Extension https://marketplace.visualstudio.com/items?itemName=vassilik.cscs-debugger