An edge connects two nodes. This edge connects node.js and .NET.
See the Edge.js overview.
Edge.js allows you to run .NET and node.js code in one process. 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. The .NET code can be pre-compiled or specified as C# source: edge.js can compile C# script at runtime.
Edge.js provides a basic, prescriptive model and implementation for interoperability between .NET and node.js in-process. You can built upon and extended this basic mechanism to support more specific scenarios, for example:
- implementing express.js handlers and connect middleware for node.js application using .NET 4.5 (read more),
- implementing CPU-bound computations in .NET and running them in-process with node.js application without blocking the event loop (read more),
- using C# and .NET instead of writing native node.js extensions in C/C++ and Win32 to access Windows specific functionality from a node.js application (read more).
Edge.js is a native node.js module for Windows. It bridges between JavaScript, native, and CLR/.NET code (think C#). The module takes care of marshaling data between V8 and CLR heaps as well as reconciling threading models. The .NET code is running in-process either asynchronously or on CLR threads while the node.js event loop remains unblocked. The .NET code can be integrated into a node.js application as C# script that will be automatically compiled, or as a pre-compiled CLR assembly.
Read more about the background and motivations of the project here.
Follow @tjanczuk for updates related to the module.
- Windows
- node.js 0.6.x or later (developed and tested with v0.6.20, v0.8.22, and v0.10.0, both x32 and x64 architectures)
- .NET 4.5
Install edge:
npm install edge
In your server.js:
var edge = require('edge');
var helloWorld = edge.func('async (input) => { return ".NET Welcomes " + input.ToString(); }');
helloWorld('JavaScript', function (error, result) {
if (error) throw error;
console.log(result);
});
Run and enjoy:
C:\projects\barebones>node server.js
.NET welcomes JavaScript
Edge provies 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 asynchronoulsy 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 synchronsouly. 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 antoher 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;
}
*/});
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(__dirname + '/add7.csx');
If you integrate C# code into your node.js application by specifying C# source using one of he 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 constucted 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 constuct 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.
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 comment pattern. For example, to use ADO.NET you must reference System.Data.dll:
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' ]
);
Edge module can marshal any JSON-serializable value between .NET and node.js. Edge also supports marshaling between node.js Buffer
instance and a .NET 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 IDictionary<string,object>
, 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 acces the data in .NET:
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Edge.Sample
{
public class Startup
{
public async Task<object> Invoke(object input)
{
IDictionary<string, object> payload = (IDictionary<string,object>)input;
int anInteger = (int)payload["anInteger"];
double aNumber = (double)payload["aNumber"];
string aString = (string)payload["aString"];
bool aBoolean = (bool)payload["aBoolean"];
byte[] aBuffer = (byte[])payload["aBuffer"];
object[] anArray = (object[])payload["anArray"];
IDictionary<string, object> anObject = (IDictionary<string,object>)payload["anObject"];
return null;
}
}
}
Similar type marshaling is applied when .NET code passes data back to node.js code. In .NET code you can provide an instance of any JSON-serializable CLR type, including domain specific types like Person
or anonymous objects. For example:
using System.Threading.Tasks;
namespace Edge.Sample
{
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(object 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 getData = edge.func('Edge.Sample.dll');
getData(null, function (error, result) {
if (error) throw error;
console.log(result);
});
Passing this .NET object to node.js generates a JavaScript object as follows:
C:\projects\barebones>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 } }
In addition to marshaling 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 multiplyBy2 = edge.func('Edge.Sample.dll');
var payload = {
someParameter: 'arbitrary parameter',
add: function (data, callback) {
callback(null, data.a + data.b);
}
};
multiplyBy2(payload, function (error, result) {
if (error) throw error;
console.log(result);
});
The .NET code implements the multiplyBy2 function. It generates two numbers, calls back into the add
function exported from node.js to add them, multiples the result by 2 in .NET, and returns the result back to node.js:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Edge.Sample
{
public class Startup
{
public async Task<object> Invoke(IDictionary<string, object> input)
{
Func<object, Task<object>> add = (Func<object, Task<object>>)input["add"];
var twoNumbers = new { a = 2, b = 3 };
var addResult = (int)await add(twoNumbers);
return addResult * 2;
}
}
}
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 parametrs. The first one is the error, if any, and the second the result of the operation:
function (payload, callback) {
var error; // must be null or undefined in the absence of error
var result;
// do something
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.
Similarly to marshaling 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 () {/*
async (input) =>
{
return (Func<object,Task<object>>)(async (i) => {
Console.WriteLine("Hello from .NET");
return null;
});
}
*/});
var hello = createHello(null, true);
hello(null, true); // prints out "Hello from .NET"
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 () {/*
async (input) =>
{
var k = (int)input;
return (Func<object,Task<object>>)(async (i) => { return ++k; });
}
*/});
var counter = createCounter(12, true); // create counter with 12 as initial state
console.log(counter(null, true)); // prints 13
console.log(counter(null, true)); // prints 14
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 applicaiton and compile it on the fly. To enable compilation of other CLR languages (e.g. F#) at runtime or to support other idioms of constructing C# script, you can use the compiler composibility model provided by edge.js. Please read the add support for a CLR language guide to get started.
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 marshaled as the error
parameter to the node.js callback function. Consider this .NET code:
public Task<object> Invoke(object input)
{
throw new Exception("Sample .NET exception");
}
And the node.js code that invokes this .NET function and re-throws the error
parameter passed to the JavaScript callback function:
var edge = require('edge');
var clrFunc = edge.func('Edge.Sample.dll');
clrFunc(null, function (error, result) {
if (error) throw error;
});
Running this node.js application shows that the CLR exception was indeed received by the node.js callback. The error
parameter contains the full stack trace including the CLR code path:
C:\projects\barebones>node sample.js
c:\projects\edge\lib\edge.js:58
edge.callClrFunc(appId, data, callback);
^
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.Excep
tion: Sample .NET exception
at Edge.Sample.Startup.Invoke(Object input) in c:\projects\barebones\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.
This node.js code invokes a .NET routine and exports the aFunctionThatThrows
JavaScript function to it:
var edge = require('edge.js');
var multiplyBy2 = edge.func('Edge.Sample.dll');
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);
});
The .NET code calls the node.js function, catches any resulting CLR exceptions, and displays them:
public async Task<object> Invoke(object input)
{
IDictionary<string, object> payload = (IDictionary<string, object>)input;
Func<object, Task<object>> aFunctionThatThrows = (Func<object, Task<object>>)payload["aFunctionThatThrows"];
try {
var aResult = await aFunctionThatThrows(null);
}
catch(Exception e)
{
Console.WriteLine(e);
}
return null;
}
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:
C:\projects\barebones>node sample.js
System.Exception: Error: Sample JavaScript error
at payload.aFunctionThatThrows (C:\projects\barebones\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()
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. This method is currently only available if you integrated a pre-compiled CLR assembly with node.js as opposed to embedding C# literals in the application. Since the node.exe process runs both native and managed code, make sure to select the appropriate language to target:
You must have Visual Studio 2012 toolset as well as Python 2.7.x and npm-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 followig command:
npm install -g node-gyp
node-gyp configure --msvs_version=2012
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.
You must run tests from a place that has csc.exe
to VS 2012 tooset on the PATH, for example the VS 2012 developer command prompt. To run the tests using the version node.js installed you your system:
npm test
This first builds a CLR assembly in C# that contains the .NET code of the tests, and then runs the tests with mocha.
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.1.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
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.
Issues? Feedback? You know what to do. Pull requests welcome.