Skip to content

agracio/edge-js

Repository files navigation

Edge.js: .NET and Node.js in-process

Actions Status Git Issues Closed Issues


This library is based on https://github.com/tjanczuk/edge all credit for original work goes to Tomasz Janczuk.


Overview

Edge.js allows you to run Node.js and .NET code in one process on Windows, macOS, and Linux

You can call .NET functions from Node.js and Node.js functions from .NET.
Edge.js takes care of marshaling data between CLR and V8. Edge.js also reconciles threading models of single-threaded V8 and multi-threaded CLR.
Edge.js ensures correct lifetime of objects on V8 and CLR heaps.
The CLR code can be pre-compiled or specified as C#, F#, Python (IronPython), or PowerShell source: Edge.js can compile CLR scripts at runtime.
Edge can be extended to support other CLR languages or DSLs.

Edge.js interop model

Edge.js provides an asynchronous, in-process mechanism for interoperability between Node.js and .NET. You can use this mechanism to:

  • script Node.js from a .NET application on Windows using .NET Framework
  • script C# from a Node.js application on Windows, macOS, and Linux using .NET Framework/.NET Core
  • use CLR multi-threading from Node.js for CPU intensive work more...
  • write native extensions to Node.js in C# instead of C/C++
  • integrate existing .NET components into Node.js applications
  • access MS SQL from Node.js using ADO.NET
  • script F# from Node.js
  • script Powershel from Node.js
  • script Python (IronPython) from Node.js

Read more about the background and motivations of the project here.

Updates

  • Support for new versions of Node.Js.
  • Support for .NET Core 3.1 - 9.x on Windows/Linux/macOS.
  • Fixes AccessViolationException when running Node.js code from C# PR #573.
  • Fixes StackOverflowException PR #566 that occurs when underlying C# code throws complex exception.
  • Fixes issues #469, #713
  • Other PRs: PR #725, PR #640
  • Multiple bug fixes and improvements to the original code.

NPM package edge-js

NuGet package EdgeJs


Electron

For use with Electron electron-edge-js

VS Code extensions

VS Code uses Electron shell, to write extensions for it using Edge.js use electron-edge-js

Quick start

Sample app that shows how to work with .NET Core using inline code and compiled C# libraries.
https://github.com/agracio/edge-js-quick-start

Pre-requisites

Node.js Support

edge-js support policy

  • Windows supports 4 latest LTS or candidate LTS releases (even numbered).
  • Windows supports up to 1 "Current" (odd numbered) release and drops it when superseeded by new LTS candidate.
  • macOS comes precompiled with same releases as Windows. When using Node.js version that is not pre-compiled edge-js binaries will be compiled during npm install using node-gyp.
  • Linux will always compile edge-js binaries during npm install using node-gyp.

Windows

Version x86 x64 arm64
18.x ✔️ ✔️
20.x ✔️ ✔️ ✔️
22.x ✔️ ✔️ ✔️
24.x ✔️ ✔️
25.x ✔️ ✔️

macOS binaries pre-compiled for

Version x64 arm64
18.x ✔️ ✔️
20.x ✔️ ✔️
22.x ✔️ ✔️
24.x ✔️ ✔️
25.x ✔️ ✔️

Supports

Version x64 arm64
16.x - 25.x ✔️ ✔️

Linux

Version x64 arm64
16.x - 25.x ✔️ ✔️

Other Linux architectures might work but have not been tested.

Scripting CLR from Node.js and Node.js from CRL

Script CLR from Node.js Script Node.js from CLR
.NET 4.5 Mono 6.x CoreCLR
Windows ✔️ ✔️* ✔️
Linux ✔️* ✔️
macOS ✔️ ✔️
.NET 4.5 Mono CoreCLR
Windows ✔️
Linux
macOS

Mono

Mono is no longer actively supported. Existing code will remain In Edge.Js but focus will be on .NET Core.

* Edge.js does not have a flag to force it to run in Mono environment on Windows, it would only be possible to run if you have Windows without .NET Framework present.
* On Linux there is a known bug that will throw exception when executing C# code under certain conditions. Use one of the following options.

  1. Set LD_PRELOAD env variable before running Edge.js with Mono
EXPORT LD_PRELOAD="libmono-2.0.so libmonosgen-2.0.so libstdc++.so.6"
  1. Set LD_PRELOAD in your Node.js app entry point using Javascript
Object.assign(process.env, {
    // Work around Mono problem: undefined symbol: mono_add_internal_call_with_flags
    LD_PRELOAD: 'libmono-2.0.so libmonosgen-2.0.so libstdc++.so.6',
});

Source: mono/mono#17079 (comment)

Edge.js uses this code in test setup:

edge-js/tools/test.js

Lines 19 to 24 in 8bab03a

if(process.platform === 'linux' && !process.env.EDGE_USE_CORECLR){
Object.assign(process.env, {
// Work around Mono problem: undefined symbol: mono_add_internal_call_with_flags
LD_PRELOAD: 'libmono-2.0.so libmonosgen-2.0.so libstdc++.so.6',
});
}

Node.js application packaging

When packaging your application using Webpack make sure that edge-js is specified as external module.

Webpack

module.exports = {
  target: 'node',
  externals: {
    'edge-js': 'commonjs2 edge-js',
    'edge-cs': 'commonjs2 edge-cs',
  },
  node: {
    __dirname: true,
    __filename: true,
  },
}

Next.js

next.config.js

const nextConfig = {
  serverExternalPackages: ['edge-js'],
}
 
module.exports = nextConfig

next.config.ts

const nextConfig: NextConfig = {
  serverExternalPackages: ['edge-js'],
};

export default nextConfig;

Node.js single executable application packaging

edge-js-pkg

Additional languages support

SQL scripting

Provides simple access to SQL without the need to write separate C# code.

Framework Platform NPM Package Language code Documentation
.NET 4.5 Windows edge-sql sql Script SQL in Node.js 🔗
CoreCLR Any edge-sql sql Script SQL in Node.js 🔗

Python (IronPython) scripting

NOTE This functionality requires IronPython 3.4

Framework Platform NPM Package Language code Documentation
.NET 4.5 Windows edge-py py Script Python in Node.js 🔗
CoreCLR Any edge-py py Script Python in Node.js 🔗

PowerShell scripting

NOTE CoreCLR requires dotnet 8

Framework Platform NPM Package Language code Documentation
.NET 4.5 Windows edge-ps ps Script PowerShell in Node.js 🔗
CoreCLR Windows edge-ps ps Script PowerShell in Node.js 🔗

F# scripting

Framework Platform NPM Package Language code Documentation
.NET 4.5 Windows edge-fs fs Script F# in Node.js 🔗
CoreCLR Windows edge-fs fs Script F# in Node.js 🔗

How to use

Scripting CLR from Node.js - full documentation

Scripting Node.js from CLR - full documentation

Scripting CLR from Node.js sample app https://github.com/agracio/edge-js-quick-start


Short guide

Inline C# code

ES5

var edge = require('edge-js');

var helloWorld = edge.func(function () {/*
    async (input) => { 
        return ".NET Welcomes " + input.ToString(); 
    }
*/});

helloWorld('JavaScript', function (error, result) {
    if (error) throw error;
    console.log(result);
});

ES6 with templated strings

var edge = require('edge-js');

var helloWorld = edge.func(`
    async (input) => { 
        return ".NET Welcomes " + input.ToString(); 
    }
`);

helloWorld('JavaScript', function (error, result) {
    if (error) throw error;
    console.log(result);
});

Passing parameters

var edge = require('edge-js');

var helloWorld = edge.func(function () {/*
    async (dynamic input) => { 
        return "Welcome " + input.name + " " + input.surname; 
    }
*/});

helloWorld({name: 'John', surname: 'Smith'}, function (error, result) {
    if (error) throw error;
    console.log(result);
});

Using C# class

var getPerson = edge.func({
    source: function () {/* 
        using System.Threading.Tasks;
        using System;

        public class Person
        {
            public Person(string name, string email, int age)
            {
                Id =  Guid.NewGuid();
                Name = name;
                Email = email;
                Age = age;
            }
            public Guid Id {get;set;}
            public string Name {get;set;}
            public string Email {get;set;}
            public int Age {get;set;}
        }

        public class Startup
        {
            public async Task<object> Invoke(dynamic input)
            {
                return new Person(input.name, input.email, input.age);
            }
        }
    */}
});

getPerson({name: 'John Smith', email: 'john.smith@myemailprovider', age: 35}, function(error, result) {
    if (error) throw error;
    console.log(result);
});

When using inline C# class code must include

public class Startup
{
    public async Task<object> Invoke(object|dynamic input)
    {
        // code
        // return results
    }
}

Using compiled assembly

// People.cs

using System;

namespace People
{
    public class Person
    {
        public Person(string name, string email, int age)
        {
            Id =  Guid.NewGuid();
            Name = name;
            Email = email;
            Age = age;
        }
        public Guid Id {get;}
        public string Name {get;}
        public string Email {get;}
        public int Age {get;}
    }
}

// EdgeJsMethods.cs

using System.Threading.Tasks;
using People;

namespace EdgeJsMethods
{
    class Methods
    {
        public async Task<object> GetPerson(dynamic input)
        {
            return await Task.Run(() => new Person(input.name, input.email, input.age));
        }
    }
}
var edge = require('edge-js');

var getPerson = edge.func({
    assemblyFile: myDll, // path to compiled .dll
    typeName: 'EdgeJsMethods.Methods',
    methodName: 'GetPerson'
});

getPerson({name: 'John Smith', email: 'john.smith@myemailprovider', age: 35}, function(error, result) {
    if (error) throw error;
    console.log(result);
});

Edge.js C# method must have the following signature

public async Task<object> MyMethod(object|dynamic input)
{
    //return results sync/async;
}

CoreCLR

  • If not set Edge.js will run as .NET 4.5 on Windows.
  • On macOS and Linux Edge.js will default to Mono if it is installed otherwise will run as CoreCLR.
  • Can be set using js code below or as an environment variable SET EDGE_USE_CORECLR=1 or EXPORT EDGE_USE_CORECLR=1 depending on your platform.
  • Must be set before var edge = require('edge-js');
// set this variable before 
// var edge = require('edge-js');

process.env.EDGE_USE_CORECLR=1

var edge = require('edge-js');

var helloWorld = edge.func(function () {/*
    async (input) => { 
        return ".NET Welcomes " + input.ToString(); 
    }
*/});

helloWorld('JavaScript', function (error, result) {
    if (error) throw error;
    console.log(result);
});

Executing synchronously without function callback

If your C# implementation will complete synchronously you can call this function as any synchronous JavaScript function as follows:

var edge = require('edge-js');

var helloWorld = edge.func(function () {/*
    async (input) => { 
        return ".NET Welcomes " + input.ToString(); 
    }
*/});

var result = helloWorld('JavaScript', true);

Calling C# asynchronous implementation as a synchronous JavaScript function will fail

var edge = require('edge-js');

var helloWorld = edge.func(function () {/*
    async (input) => { 
        return await Task.Run(() => ".NET Welcomes " + input.ToString());
    }
*/});

// sync call will throw exception
var result = helloWorld('JavaScript', true);

Promises

var func = edge.func(function () {/*
    async (dynamic input) => { 
        return "Welcome " + input.name + " " + input.surname; 
    }
*/});

function helloWorld(){
    return new Promise((resolve, reject) =>{
        func({name: 'John', surname: 'Smith'}, function (error, result) {
            if(error) reject(error);
            else resolve(result);
        });
    });
}

Scripting Node.js from CLR

using System; 
using System.Threading.Tasks;
using EdgeJs;

class Program
{
    public static async Task Start()
    {
        var func = Edge.Func(@"
            return function (data, callback) {
                callback(null, 'Node.js welcomes ' + data);
            }
        ");

        Console.WriteLine(await func(".NET"));
    }

    static void Main(string[] args)
    {
        Start().Wait();
    }
}

More examples in tests DoubleEdge.cs


Docker

Dockerfile: Dockerfile
Docker Hub image: agracio/ubuntu-node-netcore

  • Based on Ununtu 24.04
  • User directory devvol

Pre-installed packages

  • Node.js 22
  • dotnet 9
  • git
  • build tools
  • sudo, curl, wget
  • node-gyp

Using container

  • Run interactive starting in devvol, set EDGE_USE_CORECLR=1 at container level
  • Git clone edge-js and enter cloned repo directory
  • npm install
  • Run tests
docker run -w /devvol -e EDGE_USE_CORECLR=1 -it agracio/ubuntu-node-netcore:latest
git clone https://github.com/agracio/edge-js.git && cd edge-js
npm i
npm test


Edge.js readme

❗ Some of the original Edge.js documentation is outdated ❗

What problems does Edge.js solve?

Ah, whatever problem you have. If you have this problem, this solves it.

--Scott Hanselman (@shanselman)

Before you dive in

Read the Edge.js introduction on InfoQ.
Listen to the Edge.js podcast on Herdingcode.

Contents

Scripting CLR from Node.js
    What you need
        Windows
        Linux
        OSX
    How to: C# hello, world
    How to: integrate C# code into Node.js code
    How to: specify additional CLR assembly references in C# code
    How to: marshal data between C# and Node.js
    How to: call Node.js from C#
    How to: export C# function to Node.js
    How to: script Python in a Node.js application
    How to: script PowerShell in a Node.js application
    How to: script F# in a Node.js application
    How to: script Lisp in a Node.js application
    How to: script T-SQL in a Node.js application
    How to: support for other CLR languages
    How to: exceptions
    How to: app.config
    How to: debugging
    Performance
    Building on Windows
    Building on OSX
    Building on Linux
    Running tests
Scripting Node.js from CLR
    What you need
    How to: Node.js hello, world
    How to: integrate Node.js into CLR code
    How to: use Node.js built-in modules
    How to: use external Node.js modules
    How to: handle Node.js events in .NET
    How to: expose Node.js state to .NET
    How to: use Node.js in ASP.NET application
    How to: debug Node.js code running in a CLR application
    Building Edge.js NuGet package
    Running tests of scripting Node.js in C#
Use cases and other resources
Contribution and derived work

Scripting CLR from Node.js

If you are writing a Node.js application, this section explains how you include and run CLR code in your app. It works on Windows, MacOS, and Linux.

What you need

Edge.js runs on Windows, Linux, and OSX and requires supported version of Node.js 8.x, 7.x, 6.x, as well as .NET Framework 4.5 (Windows), Mono 4.2.4 (OSX, Linux), or .NET Core 1.0.0 Preview 2 (Windows, OSX, Linux).

NOTE there is a known issue with Mono after 4.2.4 that will be addressed in Mono 4.6.

Windows setup

If you have both desktop CLR and .NET Core installed, read using .NET Core for how to configure Edge to use one or the other.

Linux setup

OSX setup

How to: C# hello, world

Follow setup instructions for your platform.

Install edge:

npm install edge-js

In your server.js:

var edge = require('edge-js');

var helloWorld = edge.func(function () {/*
    async (input) => { 
        return ".NET Welcomes " + input.ToString(); 
    }
*/});

helloWorld('JavaScript', function (error, result) {
    if (error) throw error;
    console.log(result);
});

Run and enjoy:

$>node server.js
.NET welcomes JavaScript

If you want to use .NET Core as your runtime and are running in a dual runtime environment (i.e. Windows with .NET 4.5 installed as well or Linux with Mono installed), you will need to tell edge to use .NET Core by setting the EDGE_USE_CORECLR environment variable:

$>EDGE_USE_CORECLR=1 node server.js
.NET Welcomes JavaScript

How to: integrate C# code into Node.js code

Edge provides several ways to integrate C# code into a Node.js application. Regardless of the way you choose, the entry point into the .NET code is normalized to a Func<object,Task<object>> delegate. This allows Node.js code to call .NET asynchronously and avoid blocking the Node.js event loop.

Edge provides a function that accepts a reference to C# code in one of the supported representations, and returns a Node.js function which acts as a JavaScript proxy to the Func<object,Task<object>> .NET delegate:

var edge = require('edge-js');

var myFunction = edge.func(...);

The function proxy can then be called from Node.js like any asynchronous function:

myFunction('Some input', function (error, result) {
    //...
});

Alternatively, if you know the C# implementation will complete synchronously given the circumstances, you can call this function as any synchronous JavaScript function as follows:

var result = myFunction('Some input', true);

The true parameter instead of a callback indicates that Node.js expects the C# implementation to complete synchronously. If the CLR function implementation does not complete synchronously, the call above will result in an exception.

One representation of CLR code that Edge.js accepts is C# source code. You can embed C# literal representing a .NET async lambda expression implementing the Func<object, Task<object>> delegate directly inside Node.js code:

var add7 = edge.func('async (input) => { return (int)input + 7; }');

In another representation, you can embed multi-line C# source code by providing a function with a body containing a multi-line comment. Edge extracts the C# code from the function body using regular expressions:

var add7 = edge.func(function() {/*
    async (input) => {
        return (int)input + 7;
    }
*/});

Or if you use ES6 you can use template strings to define a multiline string:

var add7 = edge.func(`
    async (input) => {
        return (int)input + 7;
    }
`);

If your C# code is more involved than a simple lambda, you can specify entire class definition. By convention, the class must be named Startup and it must have an Invoke method that matches the Func<object,Task<object>> delegate signature. This method is useful if you need to factor your code into multiple methods:

var add7 = edge.func(function() {/*
    using System.Threading.Tasks;

    public class Startup
    {
        public async Task<object> Invoke(object input)
        {
            int v = (int)input;
            return Helper.AddSeven(v);
        }
    }

    static class Helper
    {
        public static int AddSeven(int v) 
        {
            return v + 7;
        }
    }
*/});

If your C# code grows substantially, it is useful to keep it in a separate file. You can save it to a file with *.csx or *.cs extension, and then reference from your Node.js application:

var add7 = edge.func(require('path').join(__dirname, 'add7.csx'));

If you integrate C# code into your Node.js application by specifying C# source using one of the methods above, edge will compile the code on the fly. If you prefer to pre-compile your C# sources to a CLR assembly, or if your C# component is already pre-compiled, you can reference a CLR assembly from your Node.js code. In the most generic form, you can specify the assembly file name, the type name, and the method name when creating a Node.js proxy to a .NET method:

var clrMethod = edge.func({
    assemblyFile: 'My.Edge.Samples.dll',
    typeName: 'Samples.FooBar.MyType',
    methodName: 'MyMethod' // This must be Func<object,Task<object>>
});

If you don't specify methodName, Invoke is assumed. If you don't specify typeName, a type name is constructed by assuming the class called Startup in the namespace equal to the assembly file name (without the .dll). In the example above, if typeName was not specified, it would default to My.Edge.Samples.Startup.

The assemblyFile is relative to the working directory. If you want to locate your assembly in a fixed location relative to your Node.js application, it is useful to construct the assemblyFile using __dirname. If you are using .NET Core, assemblyFile can also be a project name or NuGet package name that is specified in your project.json or .deps.json dependency manifest.

You can also create Node.js proxies to .NET functions specifying just the assembly name as a parameter:

var clrMethod = edge.func('My.Edge.Samples.dll');

In that case the default typeName of My.Edge.Samples.Startup and methodName of Invoke is assumed as explained above.

How to: specify additional CLR assembly references in C# code

When you provide C# source code and let edge compile it for you at runtime, edge will by default reference only mscorlib.dll and System.dll assemblies. If you're using .NET Core, we automatically reference the most recent versions of the System. Runtime, System.Threading.Tasks, and the compiler language packages, like Microsoft.CSharp. In applications that require additional assemblies you can specify them in C# code using a special hash pattern, similar to Roslyn. For example, to use ADO.NET you must reference System.Data.dll:

NOTE: #r and references: [ 'MyDll.dll' ] references only work when using .NET Framework 4.5

var add7 = edge.func(function() {/*

    #r "System.Data.dll"

    using System.Data;
    using System.Threading.Tasks;

    public class Startup
    {
        public async Task<object> Invoke(object input)
        {
            // ...
        }
    }
*/});

If you prefer, instead of using comments you can specify references by providing options to the edge.func call:

var add7 = edge.func({
    source: function() {/*

        using System.Data;
        using System.Threading.Tasks;

        public class Startup
        {
            public async Task<object> Invoke(object input)
            {
                // ...
            }
        }
    */},
    references: [ 'System.Data.dll' ]
});

If you are using .NET Core and are using the .NET Core SDK and CLI, you must have a project.json file (specification here) that specifies the dependencies for the application. This list of dependencies must also include the