A cross-platform interactive C# script editor

This is a follow up post on Using Roslyn to build a simple C# interactive script engine

In this post I will explore the possibilities of making a REPL as a cross-platform desktop application. The REPL should support various languages but initially C#.

There a several technologies available to build cross-platform applications but since I like new and shiny things a went with Electron. Electron is built on Chromium and Node.js and lets you build applications using HTML, CSS and JavaScript.

The other important component of the application is of course a script engine for the language to supply a REPL for. This is in the case of C# the scripting libraries of Roslyn which I also used in the last post.

Then there’s the question of how to tie an Electron based application to a .NET library? Enter Edge.js, a pretty amazing tool which makes it possible for different languages and technologies to “talk” to each other.

Electron talking to Edge talking to Roslyn
Electron talking to Edge talking to Roslyn


The interactive editor

An Electron app is built using HTML, CSS and Javascript and contains in its simplest form a Main.js, Index.html and a package.json file. I won’t go in to details of the anatomy of an Electron app, there’s a nice quick start guide here for the curious reader.

Here’s the main parts of Main.js:

const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
let mainWindow;

function createWindow () {
  // Create the browser window.
  mainWindow = new BrowserWindow({width: 800, height: 600});
  // and load the index.html of the app.
  mainWindow.loadURL('file://' + __dirname + '/index.html');
  // Open the DevTools.
app.on('ready', createWindow);

The package.json in its simplest form:

  "name": "electron-repl",
  "version": "1.0.0",
  "description": "Electron repl",
  "main": "main.js",
  "scripts": {
    "start": "electron main.js"

The Index.html defines a text area in which the user enters the C# script code:


Now we need some code in Index.js for capturing the user’s selected code:

var edge = require('edge')
var toScriptEngine = require('edge').func({
    assemblyFile: 'ScriptEngine.dll',
    typeName: 'ScriptEngine',
    methodName: 'Execute'

var ta = document.getElementById('ta');
ta.onkeydown = function (event) {
    if (event.defaultPrevented) {
    var handled = false;
    if (event.key !== undefined) {
        if (event.key === 'Enter' && event.altKey) {
            toScriptEngine(getSelectedText(), function (error, result) { console.log(result); });
    } else if (event.keyIdentifier !== undefined) {
        if (event.keyIdentifier === "Enter" && event.altKey) {
            toScriptEngine(getSelectedText(), function (error, result) { console.log(result); });        }

    } else if (event.keyCode !== undefined) {
        if (event.keyCode === 13 && event.altKey) {
            toScriptEngine(getSelectedText(), function (error, result) { console.log(result); });
    if (handled) {
function getSelectedText() {....}

When the user selects some code and presses `Alt + Enter` it is sent off to the REPL and evaluated. The result is logged to the console window.
The magic here is the edge-function first in index.js. It defines a function, toScriptEngine, that has a reference to a C# method, ScriptEngine.Execute, that is to be executed when invoked. The function has callback function that supplies the result and any errors.

The script engine

The C# script engine is simple as well:

public class ScriptEngine
    private static Task> scriptState = null;
    public async Task Execute(string code)
        scriptState = scriptState == null ? CSharpScript.RunAsync(code) : scriptState.Result.ContinueWithAsync(code);
        return (await scriptState).ReturnValue;

…and the project.json containing dependencies to the script library

  "frameworks": {
    "dnxcore50": {}
  "dependencies": {
    "Microsoft.CodeAnalysis.CSharp.Scripting": "1.1.1"

Building and executing

Installing Electron can be done like so:
$ npm install --save-dev electron-prebuilt

EdgeJS which is the bridge between the Electron app and .NET is installed with:
$ npm install edge-atom-shell (edge-atom-shell is a fork of Edge hacked for Electron compatibility)
EdgeJS needs to be rebuilt against Electron’s headers to work and there’s a handy node package for doing this:
$ npm install electron-rebuild --save-dev which can be executed with
Recompiling is further described here.

To assemble the Roslyn script libraries install dnvm and then dnu restore, dnu build (to produce the ScriptEngine assembly) and dnu publish to package all dependent assemblies. Now all assemblies must be copied in to the electron app working directory. The two last steps will eventually be unnecessary.

Now, executing the app with npm start will bring up the REPL in its full glory:

The C# Interactive app
The C# Interactive app

Running on Linux

The above was done on Windows. On Linux the build procedure is pretty much the same although it works fine (at the time of writing) using EdgeJS instead of edge-atom-shell for reasons unknown to me.

Source code

The source code in this post is available here


.NET Core

This Post Has 5 Comments

  1. Paul Duran

    This is really cool. Quite powerful with such a small amount of code.

    Thanks for sharing!

  2. Ahmed Zakaria

    i seem to get this error running the app from source on win10 . i’m new to electron, what does this error mean?
    ATOM_SHELL_ASAR.js:159 Uncaught Error: The specified procedure could not be found.

    the code is :
    if (!isAsar) {
    return old.apply(this, arguments);

  3. Harish

    I have tried this on Windows 7 x64 and Electron v1.4.4. But this isn’t working for me all the installation went through without any errors. But I get an error saying “ATOM_SHELL_ASAR.js:159 Uncaught Error: Module did not self-register.”. Could you please help me with this ?

Leave a Reply