From 4fb3b18868fb6b05f6c40c9206fdfbef4a457e3c Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 6 Jul 2016 18:23:25 +0100 Subject: [PATCH] Create new top-level DefaultNodeInstance concept that will soon hold the "connection draining" logic --- samples/misc/LatencyTest/Program.cs | 6 +- .../Controllers/ResizeImage.cs | 2 +- samples/misc/NodeServicesExamples/Startup.cs | 2 +- .../Configuration.cs | 49 ---------- .../Configuration/Configuration.cs | 58 ++++++++++++ .../{ => Configuration}/NodeHostingModel.cs | 0 .../Configuration/NodeServicesOptions.cs | 23 +++++ .../HostingModels/HttpNodeInstance.cs | 4 +- .../HostingModels/INodeInstance.cs | 10 ++ .../HostingModels/NodeInvocationException.cs | 2 +- .../HostingModels/NodeInvocationInfo.cs | 2 +- .../HostingModels/OutOfProcessNodeInstance.cs | 17 ++-- .../HostingModels/SocketNodeInstance.cs | 4 +- .../{INodeInstance.cs => INodeServices.cs} | 8 +- .../NodeServicesImpl.cs | 92 +++++++++++++++++++ .../NodeServicesOptions.cs | 14 --- .../Prerendering/PrerenderTagHelper.cs | 1 - .../Prerendering/Prerenderer.cs | 2 +- .../Webpack/WebpackDevMiddleware.cs | 3 +- 19 files changed, 210 insertions(+), 89 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.NodeServices/Configuration.cs create mode 100644 src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs rename src/Microsoft.AspNetCore.NodeServices/{ => Configuration}/NodeHostingModel.cs (100%) create mode 100644 src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs create mode 100644 src/Microsoft.AspNetCore.NodeServices/HostingModels/INodeInstance.cs rename src/Microsoft.AspNetCore.NodeServices/{INodeInstance.cs => INodeServices.cs} (53%) create mode 100644 src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs delete mode 100644 src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs diff --git a/samples/misc/LatencyTest/Program.cs b/samples/misc/LatencyTest/Program.cs index 855402b..2087d29 100755 --- a/samples/misc/LatencyTest/Program.cs +++ b/samples/misc/LatencyTest/Program.cs @@ -12,21 +12,21 @@ namespace ConsoleApplication public class Program { public static void Main(string[] args) { - using (var nodeServices = CreateNodeServices(Configuration.DefaultNodeHostingModel)) { + using (var nodeServices = CreateNodeServices(NodeServicesOptions.DefaultNodeHostingModel)) { MeasureLatency(nodeServices).Wait(); } } private static async Task MeasureLatency(INodeServices nodeServices) { // Ensure the connection is open, so we can measure per-request timings below - var response = await nodeServices.Invoke("latencyTest", "C#"); + var response = await nodeServices.InvokeAsync("latencyTest", "C#"); Console.WriteLine(response); // Now perform a series of requests, capturing the time taken const int requestCount = 100; var watch = Stopwatch.StartNew(); for (var i = 0; i < requestCount; i++) { - await nodeServices.Invoke("latencyTest", "C#"); + await nodeServices.InvokeAsync("latencyTest", "C#"); } // Display results diff --git a/samples/misc/NodeServicesExamples/Controllers/ResizeImage.cs b/samples/misc/NodeServicesExamples/Controllers/ResizeImage.cs index e0f498e..43f45df 100644 --- a/samples/misc/NodeServicesExamples/Controllers/ResizeImage.cs +++ b/samples/misc/NodeServicesExamples/Controllers/ResizeImage.cs @@ -46,7 +46,7 @@ namespace NodeServicesExamples.Controllers } // Invoke Node and pipe the result to the response - var imageStream = await _nodeServices.Invoke( + var imageStream = await _nodeServices.InvokeAsync( "./Node/resizeImage", fileInfo.PhysicalPath, mimeType, diff --git a/samples/misc/NodeServicesExamples/Startup.cs b/samples/misc/NodeServicesExamples/Startup.cs index cec3093..1c38c60 100755 --- a/samples/misc/NodeServicesExamples/Startup.cs +++ b/samples/misc/NodeServicesExamples/Startup.cs @@ -30,7 +30,7 @@ namespace NodeServicesExamples if (requestPath.StartsWith("/js/") && requestPath.EndsWith(".js")) { var fileInfo = env.WebRootFileProvider.GetFileInfo(requestPath); if (fileInfo.Exists) { - var transpiled = await nodeServices.Invoke("./Node/transpilation.js", fileInfo.PhysicalPath, requestPath); + var transpiled = await nodeServices.InvokeAsync("./Node/transpilation.js", fileInfo.PhysicalPath, requestPath); await context.Response.WriteAsync(transpiled); return; } diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration.cs deleted file mode 100644 index d24a7e3..0000000 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.AspNetCore.Hosting; - -namespace Microsoft.AspNetCore.NodeServices -{ - public static class Configuration - { - public const NodeHostingModel DefaultNodeHostingModel = NodeHostingModel.Http; - - private static readonly string[] DefaultWatchFileExtensions = {".js", ".jsx", ".ts", ".tsx", ".json", ".html"}; - private static readonly NodeServicesOptions DefaultOptions = new NodeServicesOptions - { - HostingModel = DefaultNodeHostingModel, - WatchFileExtensions = DefaultWatchFileExtensions - }; - - public static void AddNodeServices(this IServiceCollection serviceCollection) - => AddNodeServices(serviceCollection, DefaultOptions); - - public static void AddNodeServices(this IServiceCollection serviceCollection, NodeServicesOptions options) - { - serviceCollection.AddSingleton(typeof(INodeServices), serviceProvider => - { - var hostEnv = serviceProvider.GetRequiredService(); - if (string.IsNullOrEmpty(options.ProjectPath)) - { - options.ProjectPath = hostEnv.ContentRootPath; - } - - return CreateNodeServices(options); - }); - } - - public static INodeServices CreateNodeServices(NodeServicesOptions options) - { - var watchFileExtensions = options.WatchFileExtensions ?? DefaultWatchFileExtensions; - switch (options.HostingModel) - { - case NodeHostingModel.Http: - return new HttpNodeInstance(options.ProjectPath, /* port */ 0, watchFileExtensions); - case NodeHostingModel.Socket: - return new SocketNodeInstance(options.ProjectPath, watchFileExtensions); - default: - throw new ArgumentException("Unknown hosting model: " + options.HostingModel); - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs new file mode 100644 index 0000000..b487251 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs @@ -0,0 +1,58 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.NodeServices.HostingModels; + +namespace Microsoft.AspNetCore.NodeServices +{ + public static class Configuration + { + public static void AddNodeServices(this IServiceCollection serviceCollection) + => AddNodeServices(serviceCollection, new NodeServicesOptions()); + + public static void AddNodeServices(this IServiceCollection serviceCollection, NodeServicesOptions options) + { + serviceCollection.AddSingleton(typeof(INodeServices), serviceProvider => + { + // Since this instance is being created through DI, we can access the IHostingEnvironment + // to populate options.ProjectPath if it wasn't explicitly specified. + var hostEnv = serviceProvider.GetRequiredService(); + if (string.IsNullOrEmpty(options.ProjectPath)) + { + options.ProjectPath = hostEnv.ContentRootPath; + } + + return new NodeServicesImpl(options, () => CreateNodeInstance(options)); + }); + } + + public static INodeServices CreateNodeServices(NodeServicesOptions options) + { + return new NodeServicesImpl(options, () => CreateNodeInstance(options)); + } + + private static INodeInstance CreateNodeInstance(NodeServicesOptions options) + { + if (options.NodeInstanceFactory != null) + { + // If you've explicitly supplied an INodeInstance factory, we'll use that. This is useful for + // custom INodeInstance implementations. + return options.NodeInstanceFactory(); + } + else + { + // Otherwise we'll construct the type of INodeInstance specified by the HostingModel property, + // which itself has a useful default value. + switch (options.HostingModel) + { + case NodeHostingModel.Http: + return new HttpNodeInstance(options.ProjectPath, /* port */ 0, options.WatchFileExtensions); + case NodeHostingModel.Socket: + return new SocketNodeInstance(options.ProjectPath, options.WatchFileExtensions); + default: + throw new ArgumentException("Unknown hosting model: " + options.HostingModel); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeHostingModel.cs similarity index 100% rename from src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs rename to src/Microsoft.AspNetCore.NodeServices/Configuration/NodeHostingModel.cs diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs new file mode 100644 index 0000000..fd8b364 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs @@ -0,0 +1,23 @@ +using System; +using Microsoft.AspNetCore.NodeServices.HostingModels; + +namespace Microsoft.AspNetCore.NodeServices +{ + public class NodeServicesOptions + { + public const NodeHostingModel DefaultNodeHostingModel = NodeHostingModel.Http; + + private static readonly string[] DefaultWatchFileExtensions = { ".js", ".jsx", ".ts", ".tsx", ".json", ".html" }; + + public NodeServicesOptions() + { + HostingModel = DefaultNodeHostingModel; + WatchFileExtensions = (string[])DefaultWatchFileExtensions.Clone(); + } + + public NodeHostingModel HostingModel { get; set; } + public Func NodeInstanceFactory { get; set; } + public string ProjectPath { get; set; } + public string[] WatchFileExtensions { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs index a2aeaf1..13d7527 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -namespace Microsoft.AspNetCore.NodeServices +namespace Microsoft.AspNetCore.NodeServices.HostingModels { internal class HttpNodeInstance : OutOfProcessNodeInstance { @@ -45,7 +45,7 @@ namespace Microsoft.AspNetCore.NodeServices return result; } - public override async Task Invoke(NodeInvocationInfo invocationInfo) + protected override async Task InvokeExportAsync(NodeInvocationInfo invocationInfo) { await EnsureReady(); diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/INodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/INodeInstance.cs new file mode 100644 index 0000000..cac69f2 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/INodeInstance.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.NodeServices.HostingModels +{ + public interface INodeInstance : IDisposable + { + Task InvokeExportAsync(string moduleName, string exportNameOrNull, params object[] args); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationException.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationException.cs index 64398c4..ee056ab 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationException.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationException.cs @@ -1,6 +1,6 @@ using System; -namespace Microsoft.AspNetCore.NodeServices +namespace Microsoft.AspNetCore.NodeServices.HostingModels { public class NodeInvocationException : Exception { diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationInfo.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationInfo.cs index 2e196f6..92d96f8 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationInfo.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationInfo.cs @@ -1,4 +1,4 @@ -namespace Microsoft.AspNetCore.NodeServices +namespace Microsoft.AspNetCore.NodeServices.HostingModels { public class NodeInvocationInfo { diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index 296bb48..8018d7f 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -3,14 +3,14 @@ using System.Diagnostics; using System.IO; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.NodeServices +namespace Microsoft.AspNetCore.NodeServices.HostingModels { /// /// Class responsible for launching the Node child process, determining when it is ready to accept invocations, /// and finally killing it when the parent process exits. Also it restarts the child process if it dies. /// - /// - public abstract class OutOfProcessNodeInstance : INodeServices + /// + public abstract class OutOfProcessNodeInstance : INodeInstance { private readonly object _childProcessLauncherLock; private string _commandLineArguments; @@ -34,15 +34,12 @@ namespace Microsoft.AspNetCore.NodeServices set { _commandLineArguments = value; } } - public Task Invoke(string moduleName, params object[] args) - => InvokeExport(moduleName, null, args); - - public Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args) + public Task InvokeExportAsync(string moduleName, string exportNameOrNull, params object[] args) { - return Invoke(new NodeInvocationInfo + return InvokeExportAsync(new NodeInvocationInfo { ModuleName = moduleName, - ExportedFunctionName = exportedFunctionName, + ExportedFunctionName = exportNameOrNull, Args = args }); } @@ -53,7 +50,7 @@ namespace Microsoft.AspNetCore.NodeServices GC.SuppressFinalize(this); } - public abstract Task Invoke(NodeInvocationInfo invocationInfo); + protected abstract Task InvokeExportAsync(NodeInvocationInfo invocationInfo); protected void ExitNodeProcess() { diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs index abeeac5..f35fca2 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.NodeServices.HostingModels.VirtualConnections; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -namespace Microsoft.AspNetCore.NodeServices +namespace Microsoft.AspNetCore.NodeServices.HostingModels { internal class SocketNodeInstance : OutOfProcessNodeInstance { @@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.NodeServices _watchFileExtensions = watchFileExtensions; } - public override async Task Invoke(NodeInvocationInfo invocationInfo) + protected override async Task InvokeExportAsync(NodeInvocationInfo invocationInfo) { await EnsureReady(); var virtualConnectionClient = await GetOrCreateVirtualConnectionClientAsync(); diff --git a/src/Microsoft.AspNetCore.NodeServices/INodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/INodeServices.cs similarity index 53% rename from src/Microsoft.AspNetCore.NodeServices/INodeInstance.cs rename to src/Microsoft.AspNetCore.NodeServices/INodeServices.cs index 0c17ea1..3aa09e3 100644 --- a/src/Microsoft.AspNetCore.NodeServices/INodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/INodeServices.cs @@ -5,8 +5,14 @@ namespace Microsoft.AspNetCore.NodeServices { public interface INodeServices : IDisposable { + Task InvokeAsync(string moduleName, params object[] args); + + Task InvokeExportAsync(string moduleName, string exportedFunctionName, params object[] args); + + [Obsolete("Use InvokeAsync instead")] Task Invoke(string moduleName, params object[] args); + [Obsolete("Use InvokeExportAsync instead")] Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args); } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs b/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs new file mode 100644 index 0000000..29649fd --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs @@ -0,0 +1,92 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.NodeServices.HostingModels; + +namespace Microsoft.AspNetCore.NodeServices +{ + /// + /// Default implementation of INodeServices. This is the primary API surface through which developers + /// make use of this package. It provides simple "InvokeAsync" methods that dispatch calls to the + /// correct Node instance, creating and destroying those instances as needed. + /// + /// If a Node instance dies (or none was yet created), this class takes care of creating a new one. + /// If a Node instance signals that it needs to be restarted (e.g., because a file changed), then this + /// class will create a new instance and dispatch future calls to it, while keeping the old instance + /// alive for a defined period so that any in-flight RPC calls can complete. This latter feature is + /// analogous to the "connection draining" feature implemented by HTTP load balancers. + /// + /// TODO: Implement everything in the preceding paragraph. + /// + /// + internal class NodeServicesImpl : INodeServices + { + private NodeServicesOptions _options; + private Func _nodeInstanceFactory; + private INodeInstance _currentNodeInstance; + private object _currentNodeInstanceAccessLock = new object(); + + internal NodeServicesImpl(NodeServicesOptions options, Func nodeInstanceFactory) + { + _options = options; + _nodeInstanceFactory = nodeInstanceFactory; + } + + public Task InvokeAsync(string moduleName, params object[] args) + { + return InvokeExportAsync(moduleName, null, args); + } + + public Task InvokeExportAsync(string moduleName, string exportedFunctionName, params object[] args) + { + var nodeInstance = GetOrCreateCurrentNodeInstance(); + return nodeInstance.InvokeExportAsync(moduleName, exportedFunctionName, args); + } + + public void Dispose() + { + lock (_currentNodeInstanceAccessLock) + { + if (_currentNodeInstance != null) + { + _currentNodeInstance.Dispose(); + _currentNodeInstance = null; + } + } + } + + private INodeInstance GetOrCreateCurrentNodeInstance() + { + var instance = _currentNodeInstance; + if (instance == null) + { + lock (_currentNodeInstanceAccessLock) + { + instance = _currentNodeInstance; + if (instance == null) + { + instance = _currentNodeInstance = CreateNewNodeInstance(); + } + } + } + + return instance; + } + + private INodeInstance CreateNewNodeInstance() + { + return _nodeInstanceFactory(); + } + + // Obsolete method - will be removed soon + public Task Invoke(string moduleName, params object[] args) + { + return InvokeAsync(moduleName, args); + } + + // Obsolete method - will be removed soon + public Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args) + { + return InvokeExportAsync(moduleName, exportedFunctionName, args); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs b/src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs deleted file mode 100644 index dccd85d..0000000 --- a/src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Microsoft.AspNetCore.NodeServices -{ - public class NodeServicesOptions - { - public NodeServicesOptions() - { - HostingModel = Configuration.DefaultNodeHostingModel; - } - - public NodeHostingModel HostingModel { get; set; } - public string ProjectPath { get; set; } - public string[] WatchFileExtensions { get; set; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs index b09eaae..e0dcd29 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs @@ -37,7 +37,6 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering { _nodeServices = _fallbackNodeServices = Configuration.CreateNodeServices(new NodeServicesOptions { - HostingModel = Configuration.DefaultNodeHostingModel, ProjectPath = _applicationBasePath }); } diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs index 5e10322..40286d1 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering string requestPathAndQuery, object customDataParameter) { - return nodeServices.InvokeExport( + return nodeServices.InvokeExportAsync( NodeScript.Value.FileName, "renderToString", applicationBasePath, diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs index 8c3dbd2..959ee46 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -44,7 +44,6 @@ namespace Microsoft.AspNetCore.Builder var hostEnv = (IHostingEnvironment)appBuilder.ApplicationServices.GetService(typeof(IHostingEnvironment)); var nodeServices = Configuration.CreateNodeServices(new NodeServicesOptions { - HostingModel = Configuration.DefaultNodeHostingModel, ProjectPath = hostEnv.ContentRootPath, WatchFileExtensions = new string[] { } // Don't watch anything }); @@ -61,7 +60,7 @@ namespace Microsoft.AspNetCore.Builder suppliedOptions = options }; var devServerInfo = - nodeServices.InvokeExport(nodeScript.FileName, "createWebpackDevServer", + nodeServices.InvokeExportAsync(nodeScript.FileName, "createWebpackDevServer", JsonConvert.SerializeObject(devServerOptions)).Result; // Proxy the corresponding requests through ASP.NET and into the Node listener