Edge.js: .NET and Node.js in-process
An edge connects two nodes. This edge connects Node.js and .NET. V8 and CLR/Mono - in process. On Windows, MacOS, and Linux.
You can script C# from a Node.js process:
var edge = require('edge');
var helloWorld = edge.func(function () {});
helloWorld('JavaScript', function (error, result) {
if (error) throw error;
console.log(result);
});
You can also script Node.js from C#:
using System;
using System.Threading.Tasks;
using EdgeJs;
class Program
{
public static async void 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)
{
Task.Run((Action)Start).Wait();
}
}
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
See the Edge.js overview.
Read the Edge.js introduction on InfoQ.
Listen to the Edge.js podcast on Herdingcode.
Contents
Introduction
Scripting CLR from Node.js
What you need (Windows, Linux, MacOS, Docker)
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#
Contribution and derived work
Introduction
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 marshalling 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, or PowerShell source: Edge.js can compile CLR scripts at runtime. Edge can be extended to support other CLR languages or DSLs.
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 (console app, ASP.NET, etc.)
- script C# from a Node.js application on Windows, MacOS, and Linux
- access MS SQL from Node.js using ADO.NET more...
- 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
Read more about the background and motivations of the project here.
Follow @tjanczuk for updates related to the module.
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 Node.js 4.x, 0.12.x, 0.10.x, or 0.8.x, as well as .NET Framework 4.5 (Windows) or Mono 4.0.4.1 (OSX, Linux).
Windows
Linux
MacOS
Docker
Edge.js is available as a Docker image on the tjanczuk/edgejs repository on Docker Hub. The image is based on Ubuntu 14.04, and contains Node.js 0.10.30 x64, Mono 3.4.0 x64, and globally installed Edge.js:
> docker run -it tjanczuk/edgejs:0.9.3
> cd samples
> node 101_hello_lambda.js
.NET welcomes Node.js
How to: C# hello, world
Follow setup instructions for your platform.
Install edge:
npm install edge
In your server.js:
var edge = require('edge');
var helloWorld = edge.func(function () {});
helloWorld('JavaScript', function (error, result) {
if (error) throw error;
console.log(result);
});
Run and enjoy:
$>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');
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() {});
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() {});
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'
});
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
.
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. 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:
var add7 = edge.func(function() {});
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() {},
references: [ 'System.Data.dll' ]
);
How to: marshal data between C# and Node.js
Edge.js can marshal any JSON-serializable value between .NET and Node.js (although JSON serialization is not used in the process). Edge also supports marshalling between Node.js Buffer
instance and a CLR byte[]
array to help you efficiently pass binary data.
You can call .NET from Node.js and pass in a complex JavaScript object as follows:
var dotNetFunction = edge.func('Edge.Sample.dll');
var payload = {
anInteger: 1,
aNumber: 3.1415,
aString: 'foo',
aBoolean: true,
aBuffer: new Buffer(10),
anArray: [ 1, 'foo' ],
anObject: { a: 'foo', b: 12 }
};
dotNetFunction(payload, function (error, result) { });
In .NET, JavaScript objects are represented as dynamics (which can be cast to IDictionary<string,object>
if desired), JavaScript arrays as object[]
, and JavaScript Buffer
as byte[]
. Scalar JavaScript values have their corresponding .NET types (int
, double
, bool
, string
). Here is how you can access the data in .NET:
using System.Threading.Tasks;
public class Startup
{
public async Task<object> Invoke(dynamic input)
{
int anInteger = (int)input.anInteger;
double aNumber = (double)input.aNumber;
string aString = (string)input.aString;
bool aBoolean = (bool)input.aBoolean;
byte[] aBuffer = (byte[])input.aBuffer;
object[] anArray = (object[])input.anArray;
dynamic anObject = (dynamic)input.anObject;
return null;
}
}
Similar type marshalling is applied when .NET code passes data back to Node.js code. In .NET code you can provide an instance of any CLR type that would normally be JSON serializable, including domain specific types like Person
or anonymous objects. For example:
using System.Threading.Tasks;
public class Person
{
public int anInteger = 1;
public double aNumber = 3.1415;
public string aString = "foo";
public bool aBoolean = true;
public byte[] aBuffer = new byte[10];
public object[] anArray = new object[] { 1, "foo" };
public object anObject = new { a = "foo", b = 12 };
}
public class Startup
{
public async Task<object> Invoke(dynamic input)
{
Person person = new Person();
return person;
}
}
In your Node.js code that invokes this .NET method you can display the result object that the callback method receives:
var edge = require('edge');
var getPerson = edge.func(function () {});
getPerson(null, function (error, result) {
if (error) throw error;
console.log(result);
});
Passing this .NET object to Node.js generates a JavaScript object as follows:
$>node sample.js
{ anInteger: 1,
aNumber: 3.1415,
aString: 'foo',
aBoolean: true,
aBuffer: <Buffer 00 00 00 00 00 00 00 00 00 00>,
anArray: [ 1, 'foo' ],
anObject: { a: 'foo', b: 12 } }
When data is marshalled from .NET to Node.js, no checks for circular references are made. They will typically result in stack overflows. Make sure the object graph you are passing from .NET to Node.js is a tree and does not contain any cycles.
WINDOWS ONLY When marshalling strongly typed objects (e.g. Person) from .NET to Node.js, you can optionally tell Edge.js to observe the System.Web.Script.Serialization.ScriptIgnoreAttribute. You opt in to this behavior by setting the EDGE_ENABLE_SCRIPTIGNOREATTRIBUTE
environment variable:
set EDGE_ENABLE_SCRIPTIGNOREATTRIBUTE=1
Edge.js by default does not observe the ScriptIgnoreAttribute to avoid the associated performance cost.
How to: call Node.js from C#
In addition to marshalling data, edge can marshal proxies to JavaScript functions when invoking .NET code from Node.js. This allows .NET code to call back into Node.js.
Suppose the Node.js application passes an add
function to the .NET code as a property of an object. The function receives two numbers and returns the sum of them via the provided callback:
var edge = require('edge');
var addAndMultiplyBy2 = edge.func(function () {});
var payload = {
a: 2,
b: 3,
add: function (data, callback) {
callback(null, data.a + data.b);
}
};
addAndMultiplyBy2(payload, function (error, result) {
if (error) throw error;
console.log(result);
});
The .NET code implements the addAndMultiplyBy2 function. It extracts the two numbers passed from Node.js, calls back into the add
function exported from Node.js to add them, multiplies the result by 2 in .NET, and returns the result back to Node.js:
$>node sample.js
10
The Node.js function exported from Node.js to .NET must follow the prescriptive async pattern of accepting two parameters: payload and a callback. The callback function accepts two parameters. The first one is the error, if any, and the second the result of the operation:
function (payload, callback) {
var error;
var result;
callback(error, result);
}
The proxy to that function in .NET has the following signature:
Func<object,Task<object>>
Using TPL in CLR to provide a proxy to an asynchronous Node.js function allows the .NET code to use the convenience of the await
keyword when invoking the Node.js functionality. The example above shows the use of the await
keyword when calling the proxy of the Node.js add
method.
How to: export C# function to Node.js
Similarly to marshalling functions from Node.js to .NET, Edge.js can also marshal functions from .NET to Node.js. The .NET code can export a Func<object,Task<object>>
delegate to Node.js as part of the return value of a .NET method invocation. For example:
var createHello = edge.func(function () {});
var hello = createHello(null, true);
hello(null, true);
This mechanism in conjunction with a closure can be used to expose CLR class instances or CLR state in general to JavaScript. For example:
var createCounter = edge.func(function () {});
var counter = createCounter(12, true);
console.log(counter(null, true));
console.log(counter(null, true));
How to: script Python in a Node.js application
NOTE This functionality requires IronPython and has been tested on Windows only.
Edge.js enables you to run Python and Node.js in-process.
In addition to platform specific prerequisites you need IronPython 2.7.3 to proceed.
Hello, world
Install edge and edge-py modules:
npm install edge
npm install edge-py
In your server.js:
var edge = require('edge');
var hello = edge.func('py', function () {});
hello('Node.js', function (error, result) {
if (error) throw error;
console.log(result);
});
Run and enjoy:
$>node py.js
Python welcomes Node.js
The interop model
Your Python script must evaluate to a lambda expression that accepts a single parameter. The parameter represents marshalled input from the Node.js code. The return value of the lambda expression is passed back as the result to Node.js code. The Python script can contain constructs (e.g. Python functions) that are used in the closure of the lambda expression. The instance of the script with associated state is created when edge.func
is called in Node.js. Each call to the function referes to that instance.
The simplest echo Python script you can embed in Node.js looks like this:
lambda x: x
To say hello, you can use something like this:
lambda: x: "Hello, " + x
To maintain a running sum of numbers:
current = 0
def add(x):
global current
current = current + x
return current
lambda x: add(x)
Python in its own file
You can reference Python script stored in a *.py file instead of embedding Python code in a Node.js script.
In your hello.py file:
def hello(input):
return "Python welcomes " + input
lambda x: hello(x)
In your hello.js file:
var edge = require('edge');
var hello = edge.func('py', 'hello.py');
hello('Node.js', function (error, result) {
if (error) throw error;
console.log(result);
});
Run and enjoy:
$>node hello.js
Python welcomes Node.js
To sync or to async, that is the question
In the examples above Python script was executing asynchronously on its own thread without blocking the singleton V8 thread on which the Node.js event loop runs. This means your Node.js application remains responsive while the Python code executes in the background.
If you know your Python code is non-blocking, or if you know what you are doing, you can tell Edge.js to execute Python code on the singleton V8 thread. This will improve performance for non-blocking Python scripts embedded in a Node.js application:
var edge = require('edge');
var hello = edge.func('py', {
source: function () {},
sync: true
});
console.log(hello('Node.js', true));
The sync: true
property in the call to edge.func
tells Edge.js to execute Python code on the V8 thread as opposed to creating a new thread to run Python script on. The true
parameter in the call to hello
requests that Edge.js does in fact call the hello
function synchronously, i.e. return the result as opposed to calling a callback function.
How to: script PowerShell in a Node.js application
NOTE This functionality only works on Windows.
Edge.js enables you to run PowerShell and Node.js in-process on Windows. Edge-PS connects the PowerShell ecosystem with Node.js.
You need Windows, Node.js, .NET 4.5, and PowerShell 3.0 to proceed.
Hello, world
Install edge and edge-ps modules:
npm install edge
npm install edge-ps
In your server.js:
var edge = require('edge');
var hello = edge.func('ps', function () {});
hello('Node.js', function (error, result) {
if (error) throw error;
console.log(result[0]);
});
Run and enjoy:
C:\testEdgeps>node server
PowerShell welcomes Node.js on 05/04/2013 09:38:40
Tapping into PowerShell's ecosystem
Rather than embedding PowerShell directly, you can use PowerShell files, dot source them and even use Import-Module.
What you can do in native PowerShell works in Node.js.
Interop PowerShell and Python
Here you can reach out to IronPython from PowerShell from within Node.js on Windows. This holds true for working with JavaScript frameworks and C#.
var edge = require('edge');
var helloPowerShell = edge.func('ps', function () {});
var helloPython = edge.func('py', function () {});
helloPython('Node.js', function(error, result){
if(error) throw error;
helloPowerShell(result, function(error, result){
if(error) throw error;
console.log(result[0]);
});
});
How to: script F# in a Node.js application
NOTE This functionality has not been tested on non-Windows platforms.
This section is coming up. In the meantime please refer to Dave Thomas blog post. This has been validated on Windows only.
var edge = require('edge');
var helloFs = edge.func('fs', function () {});
helloFs('Node.js', function (error, result) {
if (error) throw error;
console.log(result);
});
How to: script Lisp in a Node.js application
NOTE This functionality has not been tested on non-Windows platforms.
The edge-lsharp extension uses LSharp to compile and run Lisp to .NET.
Install edge and edge-lsharp modules:
npm install edge
npm install edge-lsharp
In your server.js:
var edge = require('edge');
var fact = edge.func('lsharp', function(){});
fact([5], function(err, answer){
console.log(answer);
});
An LSharp filename can be passed in instead of the Lisp string/comment:
var edge = require('edge');
var lisp = edge.func('lsharp', 'lisp-func.ls');
lisp(['arg1', 'arg2'], function(err, result){
});
In Lisp you can specify either a function (as shown in the first example) or just return a value:
var edge = require('edge');
var lisp = edge.func('lsharp', '(+ 2 3)');
lisp([], function(err, answer){
console.log(answer);
});
How to: script T-SQL in a Node.js application
NOTE This functionality has only been tested on Windows. Although ADO.NET exist in Mono, your mileage can vary.
The edge-sql extension of Edge.js allows for accessing MS SQL databases by scripting T-SQL inside the Node.js application. The edge-sql extension uses async ADO.NET SQL client to access MS SQL.
You need Windows, Node.js, and .NET 4.5. To run the sample code below you also need a connection string to the sample Northwind database that ships with MS SQL.
Hello, world
Install edge and edge-sql modules:
npm install edge
npm install edge-sql
Set the connection string as an environment variable (your connection string may be different):
set EDGE_SQL_CONNECTION_STRING=Data Source=localhost;Initial Catalog=Northwind;Integrated Security=True
In your server.js:
var edge = require('edge');
var getTop10Products = edge.func('sql', function () {});
getTop10Products(null, function (error, result) {
if (error) throw error;
console.log(result);
console.log(result[0].ProductName);
console.log(result[1].ReorderLevel);
});
Run and enjoy:
C:\projects\edge\samples>node server.js
[ { ProductID: 10,
ProductName: 'New Ikura',
SupplierID: 4,
CategoryID: 8,
QuantityPerUnit: '12 - 200 ml jars',
UnitPrice: '31.000',
UnitsInStock: 31,
UnitsOnOrder: 0,
ReorderLevel: 0,
Discontinued: false },
...
]
New Ikura
12
Parameterized queries
You can construct a parameterized query once and provide parameter values on a per-call basis:
var edge = require('edge');
var getProduct = edge.func('sql', function () {});
getProduct({ myProductId: 10 }, function (error, result) {
if (error) throw error;
console.log(result);
});
Basic CRUD support: select, update, insert, delete
The four basic CRUD operations are supported. For example, here is how an update can look like:
var edge = require('edge');
var updateProductName = edge.func('sql', function () {});
updateProductName({ myProductId: 10, newName: 'New Ikura' }, function (error, result) {
if (error) throw error;
console.log(result);
});
How to: support for other CLR languages
Edge.js can work with any pre-compiled CLR assembly that contains the Func<object,Task<object>>
delegate. Out of the box, Edge.js also allows you to embed C# source code in a Node.js application and compile it on the fly.
To enable compilation of other CLR languages (e.g. F#) at runtime, or to support domain specific languages (DSLs) like T-SQL, you can use the compiler composability model provided by Edge.js. Please read the add support for a CLR language guide to get started.
How to: exceptions
Edge.js marshals Node.js errors and exceptions to .NET as well as .NET exceptions to Node.js.
CLR exceptions thrown in .NET code invoked from Node.js are marshalled as the error
parameter to the Node.js callback function. Consider this example:
var edge = require('edge');
var clrFunc = edge.func(function () {});
clrFunc(null, function (error, result) {
if (error) {
console.log('Is Error?', error instanceof Error);
console.log('-----------------');
console.log(util.inspect(error, showHidden=true, depth=99, colorize=false));
return;
}
});
Running this Node.js application shows that the CLR exception was indeed received by the Node.js callback. The error
parameter contains an Error object having most of the properties of the Exceptions copied over:
Is Error? true
-----------------
{ [System.AggregateException: One or more errors occurred.]
message: 'One or more errors occurred.',
name: 'System.AggregateException',
InnerExceptions: 'System.Collections.ObjectModel.ReadOnlyCollection`1[[System.Exception, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]',
Message: 'One or more errors occurred.',
Data: 'System.Collections.ListDictionaryInternal',
InnerException:
{ [System.Exception: Sample exception]
message: 'Sample exception',
name: 'System.Exception',
Message: 'Sample exception',
Data: 'System.Collections.ListDictionaryInternal',
TargetSite: 'System.Reflection.RuntimeMethodInfo',
StackTrace: ' at Startup.<<Invoke>b__0>d__2.MoveNext() in c:\\Users\\User.Name\\Source\\Repos\\eCash2\\test\\edge2.js:line 7\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()\r\n at Startup.<Invoke>d__4.MoveNext() in c:\\Users\\User.Name\\Source\\Repos\\eCash2\\test\\edge2.js:line 5',
Source: 'cp2luegt',
HResult: -2146233088 },
HResult: -2146233088 }
The exception is copied back as Error object like every normal result object from the .NET world to JavaScript.
Therefore all properties and their values are available on the Error object.
Additionally, the following happens during the mapping:
- To represent the Exception type, its full name is stored as
name
. - To follow the JavaScript convention for Errors, the
Message
is also stored as the property message
. System::Reflection::RuntimeMethodInfo
s are not copied to avoid stack overflows
$>node sample.js
Edge.js:58
edge.callClrFunc(appId, data, callback);
^
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
---> System.Exception: Sample exception
at Startup.Invoke(Object input) in sample.cs:line 12
JavaScript exceptions thrown in Node.js code invoked from .NET are wrapped in a CLR exception and cause the asynchronous Task<object>
to complete with a failure. Errors passed by Node.js code invoked from .NET code to the callback function's error
parameter have the same effect. Consider this example:
var edge = require('edge');
var multiplyBy2 = edge.func(function () {});
var payload = {
someParameter: 'arbitrary parameter',
aFunctionThatThrows: function (data, callback) {
throw new Error('Sample JavaScript error');
}
};
multiplyBy2(payload, function (error, result) {
if (error) throw error;
console.log(result);
});
Running the code shows the .NET code receiving a CLR exception as a result of the Node.js function throwing a JavaScript error. The exception shows the complete stack trace, including the part that executed in the Node.js code:
$>node sample.js
System.Exception: Error: Sample JavaScript error
at payload.aFunctionThatThrows (sample.js:7:11)
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Edge.Sample.Startup.<Invoke>d__0.MoveNext()
How to: app.config
When running C# code within Node.js app, the app config file is node.exe.config and should be located right next to the node.exe file.
How to: debugging
NOTE This is Windows-only functionality.
On Windows, you can debug the .NET code running as part of your Node.js application by attaching a managed code debugger (e.g. Visual Studio) to node.exe. You can debug .NET code in a pre-compiled CLR assembly as well C# literals embedded in the application and compiled by Edge.js at runtime.
Debugging pre-compiled .NET code
If you have integrated .NET code into a Node.js application using a pre-compiled CLR assembly like this:
var hello = edge.func('My.Assembly.dll');
then the best way to debug your .NET code is to attach a managed code debugger (e.g. Visual Studio) to the node.exe process. Since the node.exe process runs both native and managed code, make sure to select managed code type as target:
From there, you can set breakpoints in your .NET code and the debugger will stop when they are reached.
Debugging embedded C# code
Debugging embedded C# code (on Windows) requires that EDGE_CS_DEBUG
environment variable is set in the environment of the node.exe process:
set EDGE_CS_DEBUG=1
Without this setting (the default), Edge.js will not generate debugging information when compiling embedded C# code.
You can debug C# code embedded into a Node.js application using a reference to a *.cs or *.csx file:
var hello = edge.func('MyClass.cs');
You can also debug C# code embedded directly into a *.js file using the function comment syntax:
var hello = edge.func(function () {/*
async (input) =>
{
System.Diagnostics.Debugger.Break();
var result = ".NET welcomes " + input.ToString();
return result;
}
*/});
You cannot debug C# code embedded as a simple string literal:
var hello = edge.func('async (input) => { return 2 * (int)input; }');
After setting EDGE_CS_DEBUG=1
environment variable before starting node.exe and attaching the managed debugger to the node.exe process, you can set breakpoints in C# code (which may appear as a JavaScript comment), or use System.Diagnostics.Debugger.Break()
to break into the debugger from .NET code.
Performance
Read more about performance of Edge.js on the wiki. Here is the gist of the latency (smaller is better):
Building on Windows
You must have Visual Studio 2013 toolset, Python 2.7.x, and node-gyp installed for building.
To build and test the project against all supported versions of Node.js in x86 and x64 flavors, run the following:
tools\buildall.bat
test\testall.bat
npm run jshint
To build one of the versions of Node.js officially released by Node.js, do the following:
cd tools
build.bat release 0.10.0
Note: the Node.js version number you provide must be version number corresponding to one of the subdirectories of http://nodejs.org/dist. The command will build both x32 and x64 architectures (assuming you use x64 machine). The command will also copy the edge.node executables to appropriate locations under lib\native directory where they are looked up from at runtime. The npm install
step copies the C standard library shared DLL to the location of the edge.node for the component to be ready to go.
To build the C++\CLI native extension using the version of Node.js installed on your machine, issue the following command:
npm install -g node-gyp
node-gyp configure --msvs_version=2013
node-gyp build -debug
You can then set the EDGE_NATIVE environment variable to the fully qualified file name of the built edge.node binary. It is useful during development, for example:
set EDGE_NATIVE=C:\projects\edge\build\Debug\edge.node
You can also set the EDGE_DEBUG
environment variable to 1 to have the edge module generate debug traces to the console when it runs.
Running tests
You must have mocha installed on the system. Then:
npm test
or, from the root of the enlistment:
mocha -R spec
Node.js version targeting on Windows
NOTE this is Windows only functionality.
If you want to run tests after building against a specific version of Node.js that one of the previous builds used, issue the following command:
cd test
test.bat ia32 0.10.0
Which will run the tests using Node.js x86 v0.10.0. Similarly:
cd test
test.bat x64 0.8.22
Would run tests against Node.js 0.8.22 on x64 architecture.
Lastly, you can run jshint on the project with:
npm run jshint
Building on OSX
Prerequisities:
First build Mono 4.0.4.1 x64:
brew install https://raw.githubusercontent.com/tjanczuk/edge/master/tools/mono64.rb
Then install and build Edge.js:
brew install pkg-config
sudo npm install node-gyp -g
npm install edge
To build edge from a clone of the repository or source code:
node-gyp configure build
To build a debug build instead of release, you need to:
node-gyp configure build -debug
export EDGE_NATIVE=/Users/tomek/edge/build/Debug/edge.node
Building on Linux
These instructions were tested on Ubuntu 12.04 x64 and Debian Wheezy x64. High level, you must have Node.js x64 and Mono x64 installed on the machine before you can install Edge.js. There are two ways of getting there.
Debian, starting from a clean VM (i.e. taking the high road)
If you have a fresh Debian Wheezy x64 installation, the most convenient way of installing Edge.js with all prerequisites is by running:
export USERNAME=YourUserNameHere
sudo bash -c 'bash <(wget -qO- https://raw.githubusercontent.com/tjanczuk/edge/master/tools/debian_wheezy_clean_install.sh)'
This will do the following:
- Download Node.js v4.1.1 sources, build, and install Node.js x64
- Download Mono 4.0.4.1 sources, build, and install Mono x64
- Download and install node-gyp and mocha
- Download Edge.js sources and build x64 release
- Run Edge.js tests
This process takes about 15 minutes on a Debian Wheezy x64 running on a 4 core with 16GB RAM. If successful, your machine will have all the prerequisites to npm install edge
.
Ubuntu, starting from a clean VM (i.e. taking the high road)
If you have a fresh Ubuntu 12.04 x64 installation, the most convenient way of installing Edge.js with all prerequisites is by running:
sudo bash -c 'bash <(wget -qO- https://raw.githubusercontent.com/tjanczuk/edge/master/tools/ubuntu_12.04_clean_install.sh)'
This will do the following:
- Download Node.js v4.1.1 sources, build, and install Node.js x64
- Download Mono 4.0.4.1 sources, build, and install Mono x64
- Download and install node-gyp and mocha
- Download Edge.js sources and build x64 release
- Run Edge.js tests
This process takes about 25 minutes on a Ubuntu 12.04 x64 VM running on a 2 core VM with 4GB RAM within Fusion on a MacBook Pro. If successful, your machine will have all the prerequisites to npm install edge
.
Ubuntu, manual install
This method is adequate if you already have a Mono x64 or Node.js x64 install on the machine and need to incrementally add Edge to it.
Read through the install script and cherry pick the steps you need. Here are some gotchas:
- If you need to build Mono, make sure to run
ldconfig
afterwards, otherwise the garbage collection libraries may not load. - To make sure Mono can load the standard C library, run
sudo ln -s -f /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so
. For background on that step, see how Mono loads native libraries or just cut to the chase.
Debug build of Edge.js
To build a debug build instead of release, you need to:
node-gyp configure build -debug
export EDGE_NATIVE=/home/tomek/edge/build/Debug/edge.node
Scripting Node.js from CLR
If you are writing a CLR application (e.g. a C# console application or ASP.NET web app), this section explains how you include and run Node.js code in your app. Currently it works on Windows, but MacOS, and Linux support is coming soon.
What you need
You need Windows with:
Edge.js support for scripting Node.js ships as a NuGet Package called Edge.js
. It comes with everything you need to get started writing applications for x86 and x64 architectures. However, if you want to use additional Node.js packages from NPM, you must separately install Node.js runtime to access the NPM package manager. Edge.js NuGet package has been developed and tested with Node.js v4.1.1. Older Edge.js packages exist for prior versions of Node.js. If you choose a different version of Node.js to install NPM packages, your mileage can vary.
You can install the Edge.js NuGet package using the Visual Studio built-in NuGet package management functionality or using the stand-alone NuGet client.
How to: Node.js hello, world
Create a .NET 4.5 Console Application in Visual Studio. Add the Edge.js NuGet package to the project. Then in your code:
using System;
using System.Threading.Tasks;
using EdgeJs;
class Program
{
public static async void 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)
{
Task.Run((Action)Start).Wait();
}
}
Compile and run:
C:\project\sample\bin\Debug> sample.exe
Node.js welcomes .NET
How to: integrate Node.js code into CLR code
The Edge.js NuGet package contains a single managed assembly EdgeJs.dll
with a single class EdgeJs.Edge
exposing a single static function Func
. The function accepts a string containing code in Node.js that constructs and returns a JavaScript function. The JavaScript function must have the signature required by Edge.js's prescriptive interop pattern: it must accept one parameter and a callback, and the callback must be called with an error and one return value:
var func = Edge.Func(@"
return function (data, callback) {
callback(null, 'Hello, ' + data);
}
");
Edge.js creates a Func<object,Task<object>>
delegate in CLR that allows .NET code to call the Node.js function asynchronously. You can use the standard TPL mechanisms or the async/await keywords to conveniently await completion of the asynchornous Node.js function:
var result = await func(".NET");
Note that the Node.js code snippet is not a function definition. Instead it must create and return a function instance. This allows you to initialize and maintain encapsulated Node.js state associated with the instance of the created function. The initialization code will execute only once when you call Edge.Func
. Conceptually this is similar to defining a Node.js module that exports a single function (by returning it to the caller). For example:
var increment = Edge.Func(@"
var current = 0;
return function (data, callback) {
current += data;
callback(null, current);
}
");
Console.WriteLine(await increment(4));
Console.WriteLine(await increment(7));
Using multiline C# string literals is convenient for short Node.js code snippets, but you may want to store larger Node.js code in its own *.js
file or files.
One pattern is to store your Node.js code in a myfunc.js
file:
return function (data, callback) {
callback(null, 'Node.js welcomes ' + data);
}
And then load such file into memory with File
:
var func = Edge.Func(File.ReadAllText("myfunc.js"));
Another pattern is to define a Node.js module that itself is a function:
module.exports = function (data, callback) {
callback(null, 'Node.js welcomes ' + data);
};
And then load and return this module with a short snippet of Node.js:
var func = Edge.Func(@"return require('./../myfunc.js')");
(Note the relative location of the file).
How to: use Node.js built-in modules
You can use Node.js built-in modules out of the box. For example, you can set up a Node.js HTTP server hosted in a .NET application and call it from C#:
var createHttpServer = Edge.Func(@"
var http = require('http');
return function (port, cb) {
var server = http.createServer(function (req, res) {
res.end('Hello, world! ' + new Date());
}).listen(port, cb);
};
");
await createHttpServer(8080);
Console.WriteLine(await new WebClient().DownloadStringTaskAsync("http://localhost:8080"));
How to: use external Node.js modules
You can use external Node.js modules, for example modules installed from NPM.
Note: Most Node.js modules are written in JavaScript and will execute in Edge as-is. However, some Node.js external modules are native binary modules, rebuilt by NPM on module installation to suit your local execution environment. Native binary modules will not run in Edge unless they are rebuilt to link against the NodeJS dll that Edge uses.
To install modules from NPM, you must first install Node.js on your machine and use the npm
package manager that comes with the Node.js installation. NPM modules must be installed in the directory where your build system places the Edge.js NuGet package (most likely the same location as the rest of your application binaries), or any ancestor directory. Alternatively, you can install NPM modules globally on the machine using npm install -g
:
C:\projects\websockets> npm install ws
...
ws@0.4.31 node_modules\ws
├── tinycolor@0.0.1
├── options@0.0.5
├── nan@0.3.2
└── commander@0.6.1
You can then use the installed ws
module to create a WebSocket server inside of a .NET application:
class Program
{
public static async void Start()
{
var createWebSocketServer = Edge.Func(@"
var WebSocketServer = require('ws').Server;
return function (port, cb) {
var wss = new WebSocketServer({ port: port });
wss.on('connection', function (ws) {
ws.on('message', function (message) {
ws.send(message.toUpperCase());
});
ws.send('Hello!');
});
cb();
};
");
await createWebSocketServer(8080);
}
static void Main(string[] args)
{
Task.Run((Action)Start);
new ManualResetEvent(false).WaitOne();
}
}
This WebSocket server sends a Hello message to the client when a new connection is established, and then echos a capitalized version of every message it receives back to the client. You can test this webserver with the wscat
tool included with the ws
module. To make it convenient to use, first install the ws
module globally:
npm install ws -g
Then start the .NET application conatining the WebSocket server and establish a connection to it with wscat
:
C:\projects\websockets> wscat -c ws://localhost:8080/
connected (press CTRL+C to quit)
< Hello!
> foo
< FOO
> bar
< BAR
A self-contained Node.js WebSocket server, even if running within a .NET application, is rather unexciting. After all, the same could be accomplished with a stand-alone Node.js process. Ideally you could extablish a WebSocket server in Node.js, but handle the messages in .NET. Let's do it - read on.
How to: handle Node.js events in .NET
It is often useful to handle certain events raised by the Node.js code within .NET. For example, you may want to establish a WebSocket server in Node.js, and handle the incoming messages in the .NET part of your application. This can be accomplished by passig a .NET callback function to Node.js when the the WebSocket server is created:
class Program
{
public static async void Start()
{
var onMessage = (Func<object, Task<object>>)(async (message) =>
{
return "Received string of length " + ((string)message).Length;
});
var createWebSocketServer = Edge.Func(@"
var WebSocketServer = require('ws').Server;
return function (options, cb) {
var wss = new WebSocketServer({ port: options.port });
wss.on('connection', function (ws) {
ws.on('message', function (message) {
options.onMessage(message, function (error, result) {
if (error) throw error;
ws.send(result);
});
});
ws.send('Hello!');
});
cb();
};
");
await createWebSocketServer(new
{
port = 8080,
onMessage = onMessage
});
}
static void Main(string[] args)
{
Task.Run((Action)Start);
new ManualResetEvent(false).WaitOne();
}
}
Using wscat
, you can verify the .NET handler is indeed invoked for every websocket message:
C:\projects\websockets> wscat -c ws://localhost:8080/
connected (press CTRL+C to quit)
< Hello!
> Foo
< Received string of length 3
> FooBar
< Received string of length 6
This example shows how Edge.js can create JavaScript proxies to .NET functions and marshal calls across the V8/CLR boundary in-process. Read more about data marshaling between Node.js and CLR.
How to: expose Node.js state to .NET
In the previous example a Node.js HTTP server was created and started from .NET. Suppose at some point you want to stop the HTTP server from your .NET code. Given that all references to it are embedded within Node.js code, it is not possible. However, just as Edge.js can pass a .NET function to Node.js, it also can export a Node.js function to .NET. Moreover, that function can be implemented as a closure over Node.js state. This is how it would work:
var createHttpServer = Edge.Func(@"
var http = require('http');
return function (port, cb) {
var server = http.createServer(function (req, res) {
res.end('Hello, world! ' + new Date());
}).listen(port, function (error) {
cb(error, function (data, cb) {
server.close();
cb();
});
});
};
");
var closeHttpServer = (Func<object,Task<object>>)await createHttpServer(8080);
Console.WriteLine(await new WebClient().DownloadStringTaskAsync("http://localhost:8080"));
await closeHttpServer(null);
Notice how the createHttpServer
function, in addition to starting an HTTP server in Node.js, is also returning a .NET proxy to a JavaScript function that allows that server to be stopped.
How to: use Node.js in ASP.NET web applications
Using Node.js via Edge.js in ASP.NET web applications is no different than in a .NET console application. The Edge.js NuGet package must be referenced in your ASP.NET web application. If you are using any external Node.js modules, the entire node_modules
subdirectory structure must be binplaced to the bin
folder of you web application, and deployed that way to the server.
How to: debug Node.js code running in a CLR application
The EDGE_NODE_PARAMS
environment variable allows you to specify any options that are normally passed via command line to the node executable. This includes the --debug
options necessary to use node-inspector to debug Node.js code.
Building Edge.js NuGet package
Note This mechanism requires hardening, expect the road ahead to be bumpy.
These are unstructions for building the Edge.js NuGet package on Windows. The package will support running apps in both x86 and x64 architectures using a selected version of Node.js. The resulting NuGet package is all-inclusive with the only dependency being .NET 4.5.
Preprequisties:
- Visual Studio 2013 Update 5
- Node.js (tested with v4.1.1)
- Python 2.7.x
- node-gyp (tested with 3.0.1)
To buid the NuGet package, open the Visual Studio 2013 Developer Command Prompt and call:
tools\build_double.bat 4.1.1
(you can substitite another version of Node.js, but no other than 0.10.28 were tested).
The script takes several minutes to complete and does the following:
- builds a few helper tools in C#
- downloads sources of the selected Node.js version
- downloads nuget.exe from http://nuget.org
- builds Node.js shared library for the x86 and x64 flavor
- builds Edge.js module for the x86 and x64 flavor
- builds managed EdgeJs.dll library that bootstraps running Node.js in a CLR process and provides the Edge.Func programming model
- packs everything into a NuGet package
If everything goes well, the resulting NuGet package is located in the tools\build\nuget
directory.
Running tests of scripting Node.js in C#
There are functional tests in test\double\double_test
and stress tests in test\double\double_stress
. Before you can compile these tests, you must register the location of the built NuGet package as a local NuGet feed through the NuGet configuration manager in Visual Studio.
After you have compiled the function tests, the best way to run them is from the command line:
C:\projects\edge\test\double\double_tests\bin\Release> mstest /testcontainer:double_test.dll /noisolation
After you have compiled the stress tests, simply launch the executable, attach resource monitor to the process, and leave it running to observe stability:
C:\projects\edge\test\double\double_stress\bin\Release> double_stress.exe
Contribution and derived work
I do welcome contributions via pull request and derived work.
The edge module is intended to remain a very small component with core functionality that supports interop between .NET and Node.js. Domain specific functionality (e.g. access to SQL, writing to ETW, writing connect middleware in .NET) should be implemented as separate modules with a dependency on edge. When you have a notable derived work, I would love to know about it to include a pointer here.
More
Issues? Feedback? You know what to do. Pull requests welcome.