Overview
Target Frameworks
Platforms
Prerequisites
Installation
Usage
API
Performance
Building and Testing
Projects Using this Library
Related Concepts
Contributing
About
Jering.Javascript.NodeJS enables you to invoke javascript in NodeJS, from C#. With this ability, you can use Node.js-javascript libraries and scripts from your C# projects.
You can use this library as a replacement for the obsoleted Microsoft.AspNetCore.NodeServices.
InvokeFromFileAsync<T>
replacesINodeService
'sInvokeAsync<T>
andInvokeExportAsync<T>
.
This library is flexible - it provides both a dependency injection (DI) based API and a static API. Also, it supports invoking both in-memory and on-disk javascript.
Static API example:
string javascriptModule = @"
module.exports = (callback, x, y) => { // Module must export a function that takes a callback as its first parameter
var result = x + y; // Your javascript logic
callback(null /* If an error occurred, provide an error object or message */, result); // Call the callback when you're done.
}";
// Invoke javascript
int result = await StaticNodeJSService.InvokeFromStringAsync<int>(javascriptModule, args: new object[] { 3, 5 });
// result == 8
Assert.Equal(8, result);
DI-based API example:
string javascriptModule = @"
module.exports = (callback, x, y) => { // Module must export a function that takes a callback as its first parameter
var result = x + y; // Your javascript logic
callback(null /* If an error occurred, provide an error object or message */, result); // Call the callback when you're done.
}";
// Create an INodeJSService
var services = new ServiceCollection();
services.AddNodeJS();
ServiceProvider serviceProvider = services.BuildServiceProvider();
INodeJSService nodeJSService = serviceProvider.GetRequiredService<INodeJSService>();
// Invoke javascript
int result = await nodeJSService.InvokeFromStringAsync<int>(javascriptModule, args: new object[] { 3, 5 });
// result == 8
Assert.Equal(8, result);
- .NET Standard 2.0
- .NET Framework 4.6.1
- .NET Core 3.1
- .NET 5.0
- .NET 6.0
- Windows
- macOS
- Linux
You'll need to install NodeJS and add the NodeJS executable's directory to the Path
environment variable.
Using Package Manager:
PM> Install-Package Jering.Javascript.NodeJS
Using .NET CLI:
> dotnet add package Jering.Javascript.NodeJS
This section explains how to use this library. Topics:
Using the DI-Based API
Using the Static API
Invoking Javascript
Debugging Javascript
Configuring
Customizing Logic
Enabling Multi-Process Concurrency
First, create an INodeJSService
. You can use any DI framework that has adapters for Microsoft.Extensions.DependencyInjection.
Here, we'll use vanilla Microsoft.Extensions.DependencyInjection:
var services = new ServiceCollection();
services.AddNodeJS();
ServiceProvider serviceProvider = services.BuildServiceProvider();
INodeJSService nodeJSService = serviceProvider.GetRequiredService<INodeJSService>();
Once you've got an INodeJSService
, you can invoke javascript using its invoke methods. All invoke methods are thread-safe.
Here's one of its invoke-from-string methods:
string? result = nodeJSService.InvokeFromStringAsync<Result>("module.exports = (callback, message) => callback(null, message);", args: new[] { "success" });
Assert.Equal("success", result);
We describe all of the invoke methods in detail later on.
No clean up is required when you're done:
the NodeJS process INodeJSService
sends javascript invocations to kills itself when it detects that its parent process has died.
If you'd like to manually kill the NodeJS process, you can call INodeJSService.Dispose()
.
Once the instance is disposed, all invoke methods throw ObjectDisposedException
.
This is important to keep in mind since services.AddNodeJS()
registers INodeJSService
as a singleton (same instance injected every where).
This library provides a static alternative to the DI-based API. StaticNodeJSService
wraps an INodeJSService
, exposing most of its public members.
With the static API, you don't need to worry about creating or managing INodeJSService
. Example usage;
string result = await StaticNodeJSService
.InvokeFromStringAsync<Result>("module.exports = (callback, message) => callback(null, message);", args: new[] { "success" });
Assert.Equal("success", result);
StaticNodeJSService
's invoke methods are thread-safe.
Clean-up wise, StaticNodeJSService.DisposeServiceProvider()
kills the NodeJS process immediately.
Alternatively, the NodeJS process kills itself when it detects that its parent process has died.
Whether you use the static API or the DI-based API depends on your development needs. If you're already using DI and/or you want to mock
out INodeJSService
in your tests and/or you want to customize services, use the DI-based API. Otherwise,
the static API works fine.
We'll begin with the javascript side of things. You'll need a NodeJS module that exports either a function or an object containing functions. Exported functions can be of two forms:
These functions take a callback as their first argument, and call the callback when they're done.
The callback takes two optional arguments:
- The first argument is an error or an error message. It must be of type
Error
orstring
. - The second argument is the result. It must be a JSON-serializable type, a
string
, or astream.Readable
.
Note: this is known as an error-first callback. Such callbacks are used for error handling in NodeJS asynchronous code (check out NodeJS Event Loop for more information on asynchrony in NodeJS).
As mentioned before, you'll need a module that exports either a function or an object containing functions. This is a module that exports a valid function:
module.exports = (callback, arg1, arg2, arg3) => {
... // Do something with args
callback(null /* error */, result /* result */);
}
This is a module that exports an object containing valid functions:
module.exports = {
doSomething: (callback, arg1) => {
... // Do something with arg
callback(null, result);
},
doSomethingElse: (callback) => {
... // Do something else
callback(null, result);
}
}
If an error or error message is passed to the callback, it's sent back to the calling .NET process, where an InvocationException
is thrown.
Async functions are the second valid function form. They're syntactic sugar for the function form described in the previous section (check out Callbacks, Promises and Async/Await for a summary on how callbacks, promises and async/await are related).
This is a module that exports a valid function:
module.exports = async (arg1, arg2) => {
... // Do something with args
return result;
}
And this is a module that exports an object containing valid functions:
module.exports = {
doSomething: async (arg1, arg2, arg3, arg4) => {
... // Do something with args
// async functions can explicitly return promises
return new Promise((resolve, reject) => {
resolve(result);
});
},
doSomethingElse: async (arg1) => {
... // Do something with arg
return result;
}
}
If an error is thrown in an async function, the error message is sent back to the calling .NET process, where an InvocationException
is thrown:
module.exports = async () => {
throw new Error('error message');
}
Now that we've covered the javascript side of things, let's invoke some javascript from C#.
If you have a javascript file named exampleModule.js
(located in NodeJSProcessOptions.ProjectPath
):
module.exports = (callback, message) => callback(null, { message: message });
And a .NET class Result
:
public class Result
{
public string? Message { get; set; }
}
You can invoke the javascript using InvokeFromFileAsync<T>
:
Result? result = await nodeJSService.InvokeFromFileAsync<Result>("exampleModule.js", args: new[] { "success" });
Assert.Equal("success", result?.Message);
If you change exampleModule.js
to export an object containing functions:
module.exports = {
appendExclamationMark: (callback, message) => callback(null, { message: message + '!' }),
appendFullStop: (callback, message) => callback(null, { message: message + '.' })
}
You can invoke a specific function by specifying its name:
// Invoke appendExclamationMark
Result? result = await nodeJSService.InvokeFromFileAsync<Result>("exampleModule.js", "appendExclamationMark", args: new[] { "success" });
Assert.Equal("success!", result?.Message);
When using InvokeFromFileAsync
, NodeJS always caches the module using the .js
file's absolute path as cache identifier. This is great for
performance, since the file will not be re-read or recompiled on subsequent invocations.
You can invoke javascript in string form using InvokeFromStringAsync<T>
:
string module = "module.exports = (callback, message) => callback(null, { message: message });";
// Invoke javascript
Result? result = await nodeJSService.InvokeFromStringAsync<Result>(module, args: new[] { "success" });
Assert.Equal("success", result?.Message);
In the above example, the module string is sent to NodeJS and recompiled on every invocation.
If you're planning to invoke a module repeatedly, to avoid resending and recompiling, you'll want NodeJS to cache the module.
For that, you'll have to specify a custom cache identifier, since unlike a file, a string has no "absolute file path" for NodeJS to identify it by. Once NodeJS has cached the module, you can invoke from the NodeJS cache:
string cacheIdentifier = "exampleModule";
// Try to invoke from the NodeJS cache
(bool success, Result? result) = await nodeJSService.TryInvokeFromCacheAsync<Result>(cacheIdentifier, args: new[] { "success" });
// If the module hasn't been cached, cache it. If the NodeJS process dies and restarts, the cache will be invalidated, so always check whether success is false.
if(!success)
{
// This is a trivialized example. In practice, to avoid holding large module strings in memory, you might retrieve the module
// string from an on-disk or remote source.
string moduleString = "module.exports = (callback, message) => callback(null, { message: message });";
// Send the module string to NodeJS where it's compiled, invoked and cached.
result = await nodeJSService.InvokeFromStringAsync<Result>(moduleString, cacheIdentifier, args: new[] { "success" });
}
Assert.Equal("success", result?.Message);
The following InvokeFromStringAsync<T>
overload abstracts away the above example's operations for you.
We recommend it over the logic in the above example. If you've enabled multi-process concurrency, you must use this overload:
string module = "module.exports = (callback, message) => callback(null, { message: message });";
string cacheIdentifier = "exampleModule";
// This is a trivialized example. In practice, to avoid holding large module strings in memory, you might retrieve the module
// string from an on-disk or remote source.
Func<string> moduleFactory = () => module;
// Initially, sends only cacheIdentifier to NodeJS. If the module hasn't been cached, NodeJS lets the .NET process know.
// The .NET process then creates the module string using moduleFactory and sends it to NodeJS where it's compiled, invoked and cached.
Result? result = await nodeJSService.InvokeFromStringAsync<Result>(moduleFactory, cacheIdentifier, args: new[] { "success" });
Assert.Equal("success", result?.Message);
Like when invoking javascript from a file, if the module exports an object containing functions, you can invoke a specific function by specifying its name.
You can invoke javascript in stream form using InvokeFromStreamAsync<T>
:
// Write the module to a MemoryStream for demonstration purposes.
streamWriter.Write("module.exports = (callback, message) => callback(null, {message: message});");
streamWriter.Flush();
memoryStream.Position = 0;
Result? result = await nodeJSService.InvokeFromStreamAsync<Result>(memoryStream, args: new[] { "success" });
Assert.Equal("success", result?.Message);
InvokeFromStreamAsync
behaves like InvokeFromStringAsync
with regard to caching, refer to Invoking Javascript in String Form for details.
Why bother invoking from streams? If your module is in stream form to begin with, for example, a NetworkStream
, you avoid allocating a string. Avoiding string
allocations can improve performance.
If you're using the DI-based API, configure INodeJSService
using the .NET options pattern. For example:
var services = new ServiceCollection();
services.AddNodeJS();
// Options for the NodeJS process, here we enable debugging
services.Configure<NodeJSProcessOptions>(options => options.NodeAndV8Options = "--inspect-brk");
// Options for the INodeJSService implementation
// - HttpNodeJSService is the default INodeJSService implementation. It communicates with the NodeJS process via HTTP. Below, we set the HTTP version it uses to HTTP/2.0.
// - HttpNodeJSService extends OutOfProcessNodeJSService, an abstraction for NodeJS process management. Below we set the timeout for invocations to -1 (infinite).
services.Configure<OutOfProcessNodeJSServiceOptions>(options => options.InvocationTimeoutMS = -1);
services.Configure<HttpNodeJSServiceOptions>(options => options.Version = HttpVersion.Version20);
ServiceProvider serviceProvider = services.BuildServiceProvider();
INodeJSService nodeJSService = serviceProvider.GetRequiredService<INodeJSService>(); // Configured INodeJSService
You can find the full list of options in the API section:
Use StaticNodeJSService.Configure<T>
to configure StaticNodeJSService
:
// Options for the NodeJS process, here we enable debugging
StaticNodeJSService.Configure<NodeJSProcessOptions>(options => options.NodeAndV8Options = "--inspect-brk");
// Options for the INodeJSService implementation
// - HttpNodeJSService is the default INodeJSService implementation. It communicates with the NodeJS process via HTTP. Below, we set the HTTP version it uses to HTTP/2.0.
// - HttpNodeJSService extends OutOfProcessNodeJSService, an abstraction for NodeJS process management. Below we set the timeout for invocations to -1 (infinite).
StaticNodeJSService.Configure<OutOfProcessNodeJSServiceOptions>(options => options.InvocationTimeoutMS = -1);
StaticNodeJSService.Configure<HttpNodeJSServiceOptions>(options => options.Version = HttpVersion.Version20);
Configurations made using StaticNodeJSService.Configure<T>
only apply to javascript invocations made using the static API.
We recommend making these configurations at application startup since:
StaticNodeJSService.Configure<T>
is not thread-safe.- The NodeJS process is recreated after every
StaticNodeJSService.Configure<T>
call.
Follow these steps to debug javascript invoked using INodeJSService
:
- Add
debugger
statements to your javascript module. - Configure the following options:
NodeJSProcessOptions.NodeAndV8Options
=--inspect-brk
andOutOfProcessNodeJSServiceOptions.InvocationTimeoutMS
=-1
. - Create an
INodeJSService
(or useStaticNodeJSService
). - Call a javascript invoking method.
- Navigate to
chrome://inspect/
in Chrome. - Click "Open dedicated DevTools for Node".
- Click continue to advance to your
debugger
statements.
You can customize logic by overwriting DI services.
For example, if you'd like to customize how data sent to NodeJS is serialized/deserialized, create a custom IJsonService
implementation:
// Create a custom implementation of IJsonService
public class MyJsonService : IJsonService
{
public ValueTask<T?> DeserializeAsync<T>(Stream stream, CancellationToken cancellationToken = default)
{
... // Custom deserializetion logic
}
public Task SerializeAsync<T>(Stream stream, T value, CancellationToken cancellationToken = default)
{
... // Custom serialization logic
}
}
And overwrite IJsonService
's DI service:
var services = new ServiceCollection();
services.AddNodeJS();
// Overwrite the DI service
services.AddSingleton<IJsonService, MyJsonService>();
ServiceProvider serviceProvider = services.BuildServiceProvider();
INodeJSService nodeJSService = serviceProvider.GetRequiredService<INodeJSService>();
These are some of the services you can overwrite:
Interface | Description |
---|---|
IJsonService |
An abstraction for JSON serialization/deserialization. |
IHttpClientService |
An abstraction for HttpClient . |
INodeJSProcessFactory |
An abstraction for NodeJS process creation. |
IHttpContentFactory |
An abstraction for HttpContent creation. |
INodeJSService |
An abstraction for invoking code in NodeJS. |
IEmbeddedResourcesService |
An abstraction for reading of embedded resources. |
You can find the full list of services in NodeJSServiceCollectionExtensions.cs
.
Use StaticNodeJSService.SetServices
to customize the logic executed by StaticNodeJSService
's underlying INodeJSService
:
var services = new ServiceCollection();
services.AddNodeJS();
// Overwrite the DI service
services.AddSingleton<IJsonService, MyJsonService>();
StaticNodeJSService.SetServices(services);
We recommend only calling StaticNodeJSService.SetServices
at application startup since:
StaticNodeJSService.SetServices
is not thread-safe.- The NodeJS process is recreated after every
StaticNodeJSService.SetServices
call.
To enable multi-process concurrency, set OutOfProcessNodeJSServiceOptions.Concurrency
to Concurrency.MultiProcess
:
services.Configure<OutOfProcessNodeJSServiceOptions>(options => {
options.Concurrency = Concurrency.MultiProcess; // Concurrency.None by default
options.ConcurrencyDegree = 8; // Number of processes. Defaults to the number of logical processors on your machine.
);
(see Configuring for more information on configuring)
Invocations will be distributed among multiple NodeJS processes using round-robin load balancing.
Multi-process concurrency speeds up CPU-bound workloads. We ran a benchmark executing the following logic 25-times, concurrently in NodeJS:
// Minimal CPU-bound operation
module.exports = (callback) => {
// Block CPU
var end = new Date().getTime() + 100; // 100ms block
while (new Date().getTime() < end) { /* do nothing */ }
callback(null);
};
The logic fully utilizes a CPU for 100ms.
With multi-process concurrency disabled, a single NodeJS process performs invocations synchronously, so the benchmark takes ~2500ms (25 tasks x 100ms).
With multi-process concurrency enabled, on an 8-core machine, the benchmark takes ~400ms ((25 tasks x 100ms) / 8 + overhead).
View the full results of our multi-process concurrency benchmark here.
-
You can't use multi-process concurrency if your logic persists data between invocations. For example:
const string javascriptModule = @" var lastResult; module.exports = (callback, x) => { var result = x + (lastResult ? lastResult : 0); // Use persisted value here lastResult = result; // Persist callback(null, result); }"; // result == 3 int result = await StaticNodeJSService.InvokeFromStringAsync<int>(javascriptModule, "customIdentifier", args: new object[] { 3 }); // Intended for result == 8, but result == 5 since different processes perform the invocations result = await StaticNodeJSService.InvokeFromStringAsync<int>(javascriptModule, "customIdentifier", args: new object[] { 5 });
-
With concurrency enabled, you can't use the following caching pattern (previously described in Inoke Javascript in String Form):
string cacheIdentifier = "exampleModule"; // If you have an even number of NodeJS processes, success will always be false since the resulting caching attempt is // sent to the next NodeJS process. (bool success, Result? result) = await nodeJSService.TryInvokeFromCacheAsync<Result>(cacheIdentifier, args: new[] { "success" }); // False, so we attempt to cache if(!success) { string moduleString = "module.exports = (callback, message) => callback(null, { message: message });"; // Because of round-robin load balancing, this caching attempt is sent to the next NodeJS process. result = await nodeJSService.InvokeFromStringAsync<Result>(moduleString, cacheIdentifier, args: new[] { "success" }); } Assert.Equal("success", result?.Message);
Instead, call an overload that takes a
moduleFactory
argument. These overloads atomically handle caching and invoking:string module = "module.exports = (callback, message) => callback(null, { message: message });"; string cacheIdentifier = "exampleModule"; // This is a trivialized example. In practice, to avoid holding large module strings in memory, you might retrieve the module // string from an on-disk or remote source. Func<string> moduleFactory = () => module; // Initially, sends only cacheIdentifier to NodeJS. If the module hasn't been cached, NodeJS lets the .NET process know. // The .NET process then creates the module string using moduleFactory and sends it to *the same* NodeJS process where it's compiled, invoked and cached. Result? result = await nodeJSService.InvokeFromStringAsync<Result>(moduleFactory, cacheIdentifier, args: new[] { "success" }); Assert.Equal("success", result?.Message);
Invokes a function from a NodeJS module on disk.
Task<T?> InvokeFromFileAsync<T>(string modulePath, [string? exportName = null], [object?[]? args = null], [CancellationToken cancellationToken = default(CancellationToken)])
T
The type of value returned. This may be a JSON-serializable type, string
, or Stream
.
modulePath string
The path to the module relative to NodeJSProcessOptions.ProjectPath
. This value must not be null
, whitespace or an empty string.
exportName string
The name of the function in module.exports
to invoke. If this value is null
, module.exports
is assumed to be a function and is invoked.
args object[]
The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null
, no arguments are passed.
cancellationToken CancellationToken
The cancellation token for the asynchronous operation.
The Task
representing the asynchronous operation.
ArgumentException
Thrown if modulePath
is null
, whitespace or an empty string.
ConnectionException
Thrown if unable to connect to NodeJS.
InvocationException
Thrown if the invocation request times out.
InvocationException
Thrown if a NodeJS error occurs.
ObjectDisposedException
Thrown if this instance is disposed or if it attempts to use a disposed dependency.
OperationCanceledException
Thrown if cancellationToken
is cancelled.
To avoid rereads and recompilations on subsequent invocations, NodeJS caches the module using the its absolute path as cache identifier.
If we have a file named exampleModule.js (located in NodeJSProcessOptions.ProjectPath
), with contents:
module.exports = (callback, message) => callback(null, { resultMessage: message });
Using the class Result
:
public class Result
{
public string? Message { get; set; }
}
The following assertion will pass:
Result? result = await nodeJSService.InvokeFromFileAsync<Result>("exampleModule.js", args: new[] { "success" });
Assert.Equal("success", result?.Message);
Invokes a function from a NodeJS module on disk.
Task InvokeFromFileAsync(string modulePath, [string? exportName = null], [object?[]? args = null], [CancellationToken cancellationToken = default(CancellationToken)])
modulePath string
The path to the module relative to NodeJSProcessOptions.ProjectPath
. This value must not be null
, whitespace or an empty string.
exportName string
The name of the function in module.exports
to invoke. If this value is null
, module.exports
is assumed to be a function and is invoked.
args object[]
The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null
, no arguments are passed.
cancellationToken CancellationToken
The cancellation token for the asynchronous operation.
ArgumentException
Thrown if modulePath
is null
, whitespace or an empty string.
ConnectionException
Thrown if unable to connect to NodeJS.
InvocationException
Thrown if the invocation request times out.
InvocationException
Thrown if a NodeJS error occurs.
ObjectDisposedException
Thrown if this instance is disposed or if it attempts to use a disposed dependency.
OperationCanceledException
Thrown if cancellationToken
is cancelled.
To avoid rereads and recompilations on subsequent invocations, NodeJS caches the module using the its absolute path as cache identifier.
Invokes a function from a NodeJS module in string form.
Task<T?> InvokeFromStringAsync<T>(string moduleString, [string? cacheIdentifier = null], [string? exportName = null], [object?[]? args = null], [CancellationToken cancellationToken = default(CancellationToken)])
T
The type of value returned. This may be a JSON-serializable type, string
, or Stream
.
moduleString string
The module in string form. This value must not be null
, whitespace or an empty string.
cacheIdentifier string
The module's cache identifier. If this value is null
, NodeJS ignores its module cache..
exportName string
The name of the function in module.exports
to invoke. If this value is null
, module.exports
is assumed to be a function and is invoked.
args object[]
The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null
, no arguments are passed.
cancellationToken CancellationToken
The cancellation token for the asynchronous operation.
The Task
representing the asynchronous operation.
ArgumentException
Thrown if moduleString
is null
, whitespace or an empty string.
ConnectionException
Thrown if unable to connect to NodeJS.
InvocationException
Thrown if the invocation request times out.
InvocationException
Thrown if a NodeJS error occurs.
ObjectDisposedException
Thrown if this instance is disposed or if it attempts to use a disposed dependency.
OperationCanceledException
Thrown if cancellationToken
is cancelled.
If cacheIdentifier
is null
, sends moduleString
to NodeJS where it's compiled it for one-time use.
If cacheIdentifier
isn't null
, sends both moduleString
and cacheIdentifier
to NodeJS. NodeJS reuses the module if it's already cached. Otherwise, it compiles and caches the module.
Once the module is cached, you may use INodeJSService.TryInvokeFromCacheAsync<T>
to invoke directly from the cache, avoiding the overhead of sending moduleString
.
Using the class Result
:
public class Result
{
public string? Message { get; set; }
}
The following assertion will pass:
Result? result = await nodeJSService.InvokeFromStringAsync<Result>("module.exports = (callback, message) => callback(null, { resultMessage: message });",
args: new[] { "success" });
Assert.Equal("success", result?.Message);
Invokes a function from a NodeJS module in string form.
Task InvokeFromStringAsync(string moduleString, [string? cacheIdentifier = null], [string? exportName = null], [object?[]? args = null], [CancellationToken cancellationToken = default(CancellationToken)])
moduleString string
The module in string form. This value must not be null
, whitespace or an empty string.
cacheIdentifier string
The module's cache identifier. If this value is null
, NodeJS ignores its module cache..
exportName string
The name of the function in module.exports
to invoke. If this value is null
, module.exports
is assumed to be a function and is invoked.
args object[]
The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null
, no arguments are passed.
cancellationToken CancellationToken
The cancellation token for the asynchronous operation.
The Task
representing the asynchronous operation.
ArgumentException
Thrown if moduleString
is null
, whitespace or an empty string.
ConnectionException
Thrown if unable to connect to NodeJS.
InvocationException
Thrown if the invocation request times out.
InvocationException
Thrown if a NodeJS error occurs.
ObjectDisposedException
Thrown if this instance is disposed or if it attempts to use a disposed dependency.
OperationCanceledException
Thrown if cancellationToken
is cancelled.
If cacheIdentifier
is null
, sends moduleString
to NodeJS where it's compiled for one-time use.
If cacheIdentifier
isn't null
, sends both moduleString
and cacheIdentifier
to NodeJS. NodeJS reuses the module if it's already cached. Otherwise, it compiles and caches the module.
Once the module is cached, you may use INodeJSService.TryInvokeFromCacheAsync<T>
to invoke directly from the cache, avoiding the overhead of sending moduleString
.
Invokes a function from a NodeJS module in string form.
Task<T?> InvokeFromStringAsync<T>(Func<string> moduleFactory, string cacheIdentifier, [string? exportName = null], [object?[]? args = null], [CancellationToken cancellationToken = default(CancellationToken)])
T
The type of value returned. This may be a JSON-serializable type, string
, or Stream
.
moduleFactory Func<string>
The factory that creates the module string. This value must not be null
and it must not return null
, whitespace or an empty string.
cacheIdentifier string
The module's cache identifier. This value must not be null
.
exportName string
The name of the function in module.exports
to invoke. If this value is null
, module.exports
is assumed to be a function and is invoked.
args object[]
The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null
, no arguments are passed.
cancellationToken CancellationToken
The cancellation token for the asynchronous operation.
The Task
representing the asynchronous operation.
ArgumentNullException
Thrown if module is not cached but moduleFactory
is null
.
ArgumentNullException
Thrown if cacheIdentifier
is null
.
ArgumentException
Thrown if moduleFactory
returns null
, whitespace or an empty string.
ConnectionException
Thrown if unable to connect to NodeJS.
InvocationException
Thrown if the invocation request times out.
InvocationException
Thrown if a NodeJS error occurs.
ObjectDisposedException
Thrown if this instance is disposed or if it attempts to use a disposed dependency.
OperationCanceledException
Thrown if cancellationToken
is cancelled.
Initially, sends only cacheIdentifier
to NodeJS. NodeJS reuses the module if it's already cached. Otherwise, it informs the .NET process that the module isn't cached.
The .NET process then creates the module string using moduleFactory
and send it to NodeJS where it's compiled, invoked and cached.
If exportName
is null
, module.exports
is assumed to be a function and is invoked. Otherwise, invokes the function named exportName
in module.exports
.
Invokes a function from a NodeJS module in string form.
Task InvokeFromStringAsync(Func<string> moduleFactory, string cacheIdentifier, [string? exportName = null], [object?[]? args = null], [CancellationToken cancellationToken = default(CancellationToken)])
moduleFactory Func<string>
The factory that creates the module string. This value must not be null
and it must not return null
, whitespace or an empty string.
cacheIdentifier string
The module's cache identifier. This value must not be null
.
exportName string
The name of the function in module.exports
to invoke. If this value is null
, module.exports
is assumed to be a function and is invoked.
args object[]
The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null
, no arguments are passed.
cancellationToken CancellationToken
The cancellation token for the asynchronous operation.
The Task
representing the asynchronous operation.
ArgumentNullException
Thrown if module is not cached but moduleFactory
is null
.
ArgumentNullException
Thrown if cacheIdentifier
is null
.
ArgumentException
Thrown if moduleFactory
returns null
, whitespace or an empty string.
ConnectionException
Thrown if unable to connect to NodeJS.
InvocationException
Thrown if the invocation request times out.
InvocationException
Thrown if a NodeJS error occurs.
ObjectDisposedException
Thrown if this instance is disposed or if it attempts to use a disposed dependency.
OperationCanceledException
Thrown if cancellationToken
is cancelled.
Initially, sends only cacheIdentifier
to NodeJS. NodeJS reuses the module if it's already cached. Otherwise, it informs the .NET process that the module isn't cached.
The .NET process then creates the module string using moduleFactory
and send it to NodeJS where it's compiled, invoked and cached.
If exportName
is null
, module.exports
is assumed to be a function and is invoked. Otherwise, invokes the function named exportName
in module.exports
.
Invokes a function from a NodeJS module in stream form.
Task<T?> InvokeFromStreamAsync<T>(Stream moduleStream, [string? cacheIdentifier = null], [string? exportName = null], [object?[]? args = null], [CancellationToken cancellationToken = default(CancellationToken)])
T
The type of value returned. This may be a JSON-serializable type, string
, or Stream
.
moduleStream Stream
The module in stream form. This value must not be null
.
cacheIdentifier string
The module's cache identifier. If this value is null
, NodeJS ignores its module cache..
exportName string
The name of the function in module.exports
to invoke. If this value is null
, module.exports
is assumed to be a function and is invoked.
args object[]
The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null
, no arguments are passed.
cancellationToken CancellationToken
The cancellation token for the asynchronous operation.
The Task
representing the asynchronous operation.
ArgumentException
Thrown if moduleStream
is null
.
ConnectionException
Thrown if unable to connect to NodeJS.
InvocationException
Thrown if the invocation request times out.
InvocationException
Thrown if a NodeJS error occurs.
ObjectDisposedException
Thrown if this instance is disposed or if it attempts to use a disposed dependency.
OperationCanceledException
Thrown if cancellationToken
is cancelled.
If cacheIdentifier
is null
, sends the stream to NodeJS where it's compiled for one-time use.
If cacheIdentifier
isn't null
, sends both the stream and cacheIdentifier
to NodeJS. NodeJS reuses the module if it's already cached. Otherwise, it compiles and caches the module.
Once the module is cached, you may use INodeJSService.TryInvokeFromCacheAsync<T>
to invoke directly from the cache, avoiding the overhead of sending the module stream.
Using the class Result
:
public class Result
{
public string? Message { get; set; }
}
The following assertion will pass:
using (var memoryStream = new MemoryStream())
using (var streamWriter = new StreamWriter(memoryStream))
{
// Write the module to a MemoryStream for demonstration purposes.
streamWriter.Write("module.exports = (callback, message) => callback(null, {resultMessage: message});");
streamWriter.Flush();
memoryStream.Position = 0;
Result? result = await nodeJSService.InvokeFromStreamAsync<Result>(memoryStream, args: new[] { "success" });
Assert.Equal("success", result?.Message);
}
Invokes a function from a NodeJS module in stream form.
Task InvokeFromStreamAsync(Stream moduleStream, [string? cacheIdentifier = null], [string? exportName = null], [object?[]? args = null], [CancellationToken cancellationToken = default(CancellationToken)])
moduleStream Stream
The module in stream form. This value must not be null
.
cacheIdentifier string
The module's cache identifier. If this value is null
, NodeJS ignores its module cache..
exportName string
The name of the function in module.exports
to invoke. If this value is null
, module.exports
is assumed to be a function and is invoked.
args object[]
The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null
, no arguments are passed.
cancellationToken CancellationToken
The cancellation token for the asynchronous operation.
The Task
representing the asynchronous operation.
ArgumentException
Thrown if moduleStream
is null
.
ConnectionException
Thrown if unable to connect to NodeJS.
InvocationException
Thrown if the invocation request times out.
InvocationException
Thrown if a NodeJS error occurs.
ObjectDisposedException
Thrown if this instance is disposed or if it attempts to use a disposed dependency.
OperationCanceledException
Thrown if cancellationToken
is cancelled.
If cacheIdentifier
is null
, sends the stream to NodeJS where it's compiled for one-time use.
If cacheIdentifier
isn't null
, sends both the stream and cacheIdentifier
to NodeJS. NodeJS reuses the module if it's already cached. Otherwise, it compiles and caches the module.
Once the module is cached, you may use INodeJSService.TryInvokeFromCacheAsync<T>
to invoke directly from the cache, avoiding the overhead of sending the module stream.
Invokes a function from a NodeJS module in stream form.
Task<T?> InvokeFromStreamAsync<T>(Func<Stream> moduleFactory, string cacheIdentifier, [string? exportName = null], [object?[]? args = null], [CancellationToken cancellationToken = default(CancellationToken)])
T
The type of value returned. This may be a JSON-serializable type, string
, or Stream
.
moduleFactory Func<Stream>
The factory that creates the module stream. This value must not be null
and it must not return null
.
cacheIdentifier string
The module's cache identifier. This value must not be null
.
exportName string
The name of the function in module.exports
to invoke. If this value is null
, module.exports
is assumed to be a function and is invoked.
args object[]
The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null
, no arguments are passed.
cancellationToken CancellationToken
The cancellation token for the asynchronous operation.
The Task
representing the asynchronous operation.
ArgumentNullException
Thrown if module is not cached but moduleFactory
is null
.
ArgumentNullException
Thrown if cacheIdentifier
is null
.
ArgumentException
Thrown if moduleFactory
returns null
.
ConnectionException
Thrown if unable to connect to NodeJS.
InvocationException
Thrown if the invocation request times out.
InvocationException
Thrown if a NodeJS error occurs.
ObjectDisposedException
Thrown if this instance is disposed or if it attempts to use a disposed dependency.
OperationCanceledException
Thrown if cancellationToken
is cancelled.
Initially, sends only cacheIdentifier
to NodeJS. NodeJS reuses the module if it's already cached. Otherwise, it informs the .NET process that the module isn't cached.
The .NET process then creates the module stream using moduleFactory
and send it to NodeJS where it's compiled, invoked and cached.
If exportName
is null
, module.exports
is assumed to be a function and is invoked. Otherwise, invokes the function named exportName
in module.exports
.
Invokes a function from a NodeJS module in stream form.
Task InvokeFromStreamAsync(Func<Stream> moduleFactory, string cacheIdentifier, [string? exportName = null], [object?[]? args = null], [CancellationToken cancellationToken = default(CancellationToken)])
moduleFactory Func<Stream>
The factory that creates the module stream. This value must not be null
and it must not return null
.
cacheIdentifier string
The module's cache identifier. This value must not be null
.
exportName string
The name of the function in module.exports
to invoke. If this value is null
, module.exports
is assumed to be a function and is invoked.
args object[]
The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null
, no arguments are passed.
cancellationToken CancellationToken
The cancellation token for the asynchronous operation.
The Task
representing the asynchronous operation.
ArgumentNullException
Thrown if module is not cached but moduleFactory
is null
.
ArgumentNullException
Thrown if cacheIdentifier
is null
.
ArgumentException
Thrown if moduleFactory
returns null
.
ConnectionException
Thrown if unable to connect to NodeJS.
InvocationException
Thrown if the invocation request times out.
InvocationException
Thrown if a NodeJS error occurs.
ObjectDisposedException
Thrown if this instance is disposed or if it attempts to use a disposed dependency.
OperationCanceledException
Thrown if cancellationToken
is cancelled.
Initially, sends only cacheIdentifier
to NodeJS. NodeJS reuses the module if it's already cached. Otherwise, it informs the .NET process that the module isn't cached.
The .NET process then creates the module stream using moduleFactory
and send it to NodeJS where it's compiled, invoked and cached.
If exportName
is null
, module.exports
is assumed to be a function and is invoked. Otherwise, invokes the function named exportName
in module.exports
.
Attempts to invoke a function from a module in NodeJS's cache.
Task<(bool, T?)> TryInvokeFromCacheAsync<T>(string cacheIdentifier, [string? exportName = null], [object?[]? args = null], [CancellationToken cancellationToken = default(CancellationToken)])
T
The type of value returned. This may be a JSON-serializable type, string
, or Stream
.
cacheIdentifier string
The module's cache identifier. This value must not be null
.
exportName string
The name of the function in module.exports
to invoke. If this value is null
, module.exports
is assumed to be a function and is invoked.
args object[]
The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null
, no arguments are passed.
cancellationToken CancellationToken
The cancellation token for the asynchronous operation.
The Task
representing the asynchronous operation. On completion, the task returns a (bool, T) with the bool set to true on
success and false otherwise.
ArgumentNullException
Thrown if cacheIdentifier
is null
.
ConnectionException
Thrown if unable to connect to NodeJS.
InvocationException
Thrown if the invocation request times out.
InvocationException
Thrown if a NodeJS error occurs.
ObjectDisposedException
Thrown if this instance is disposed or if it attempts to use a disposed dependency.
OperationCanceledException
Thrown if cancellationToken
is cancelled.
Using the class Result
:
public class Result
{
public string? Message { get; set; }
}
The following assertion will pass:
// Cache the module
string cacheIdentifier = "exampleModule";
await nodeJSService.InvokeFromStringAsync<Result>("module.exports = (callback, message) => callback(null, { resultMessage: message });",
cacheIdentifier,
args: new[] { "success" });
// Invoke from cache
(bool success, Result? result) = await nodeJSService.TryInvokeFromCacheAsync<Result>(cacheIdentifier, args: new[] { "success" });
Assert.True(success);
Assert.Equal("success", result?.Message);
Attempts to invoke a function from a module in NodeJS's cache.
Task<bool> TryInvokeFromCacheAsync(string cacheIdentifier, [string? exportName = null], [object?[]? args = null], [CancellationToken cancellationToken = default(CancellationToken)])
cacheIdentifier string
The module's cache identifier. This value must not be null
.
exportName string
The name of the function in module.exports
to invoke. If this value is null
, module.exports
is assumed to be a function and is invoked.
args object[]
The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null
, no arguments are passed.
cancellationToken CancellationToken
The cancellation token for the asynchronous operation.
The Task
representing the asynchronous operation. On completion, the task returns true on success and false otherwise.
ArgumentNullException
Thrown if cacheIdentifier
is null
.
ConnectionException
Thrown if unable to connect to NodeJS.
InvocationException
Thrown if the invocation request times out.
InvocationException
Thrown if a NodeJS error occurs.
ObjectDisposedException
Thrown if this instance is disposed or if it attempts to use a disposed dependency.
OperationCanceledException
Thrown if cancellationToken
is cancelled.
Moves subsequent invocations to a new NodeJS process.
ValueTask MoveToNewProcessAsync()
The ValueTask
representing the asynchronous operation.
This method exposes the system used by file watching (see OutOfProcessNodeJSServiceOptions.EnableFileWatching
) and process retries
(see OutOfProcessNodeJSServiceOptions.NumProcessRetries
) to move to new processes.
When is access to this system useful? Consider the situation where your application uses file watching. If your application knows when files change (e.g. your application is the actor changing files) you can manually invoke this method instead of using file watching. This enables you to avoid the overhead of file watching.
You do not need to await this method. Subsequent invocations are wait asynchronously until the new process is ready.
The method respects OutOfProcessNodeJSServiceOptions.GracefulProcessShutdown
.
public NodeJSProcessOptions()
The base path for resolving NodeJS module paths.
public string ProjectPath { get; set; }
If this value is null
, whitespace or an empty string and the application is an ASP.NET Core application,
project path is IHostEnvironment.ContentRootPath
.
The value used to locate the NodeJS executable.
public string? ExecutablePath { get; set; }
This value may be an absolute path, a relative path, or a file name.
If this value is a relative path, the executable's path is resolved relative to Directory.GetCurrentDirectory
.
If this value is a file name, the executable's path is resolved using the path environment variable.
If this value is null
, whitespace or an empty string, it is overridden with the file name "node".
Defaults to null
.
NodeJS and V8 options in the form <NodeJS options> <V8 options>.
public string? NodeAndV8Options { get; set; }
You can find the full list of NodeJS options here.
The NodeJS server will listen on this port.
public int Port { get; set; }
If this value is 0, the OS will choose the port.
Defaults to 0.
The NodeJS process's environment variables.
public IDictionary<string, string> EnvironmentVariables { get; set; }
You can configure NodeJS by specifying environment variables for it. Find the full list of environment variables here.
If this value doesn't contain an element with key "NODE_ENV" and the application is an ASP.NET Core application,
an element with key "NODE_ENV" is added. The added element's value is "development" if IHostEnvironment.EnvironmentName
is Environments.Development
,
and "production" otherwise.
public OutOfProcessNodeJSServiceOptions()
The maximum duration to wait for the NodeJS process to connect.
public int ConnectionTimeoutMS { get; set; }
If this value is negative, the maximum duration is infinite.
Defaults to 5000
.
The maximum duration to wait for responses to invocations.
public int InvocationTimeoutMS { get; set; }
If this value is negative, the maximum duration is infinite.
Defaults to 100,000
.
The number of times a NodeJS process retries an invocation.
public int NumRetries { get; set; }