It’s 2019, and no one can argue that to be a successful developer, you need a couple of tools in your toolbox. For ASP.NET developers, they can build web applications and run Kestrel as a web server. For front end developers, many UI frameworks have their own hot-reload capable web servers. In this post, I’ll show you .NET developers how to start a front end web server from their .NET Core process and use that in their C# application.

Download the project at GitHub.

Why Run NPM Scripts From .NET Core

Like I mentioned above, many web frameworks come with their web servers. By using these internal webservers, developers get access to many features they wouldn’t otherwise. Some highlights include hot-reloading when development files change and extra debugging capabilities. Running these servers from .NET Core allows us to read the output and create variables for our .NET Core application to use (as you’ll see later).

Before we get started, we’ll need NodeJs, NPM, and .NET Core.

Setting up the Script

In a new .NET Core application, we’ll need to set up a packages.json file. This file will have our development dependency and UI framework of our choice. In this example, I am using Parcel to bundle and minify my assets. And as it stands today, it also has [an internal webserver][parcel-sever]. We’ll use the server to serve those assets to our C# application.

{
  "name": "ChildProcesses",
  "version": "1.0.0",
  "dependencies": {},
  "devDependencies": {
    "parcel": "^1.12.4"
  },
  "scripts": {
    "dev": "parcel ./src/index.html",
    "build": "parcel build ./src/index.html",
    "watch": "parcel watch ./src/index.html"
  }
}

Parcel has three commands that are of interest to us.

  • build: creates our assets for production
  • watch: creates our assets for development and rebuilds them when they change.
  • dev: like watch but with a web server

We could run any of the scripts from our .NET Core application, but likely to run dev or watch during our development process. If you want to run build, I suggest my previous post, which shows how you can run NPM scripts during the build process.

The .NET Core Application

Let’s look at the final product of running the NPM script in our .NET Core application.

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

namespace ChildProcesses
{
    static class Program
    {
        static async Task Main(string[] args)
        {
            using var parcel = new NpmScript();

            await parcel.RunAsync(Console.WriteLine);

            Console.WriteLine(parcel.HasServer
                ? $"From ASP.NET Core. Parcel is started ({parcel.HasServer}) @ {parcel.Url} at process: {parcel.ProcessId}"
                : "Script has executed.");

            await Task.Delay(TimeSpan.FromSeconds(4));
        }
    }
}

In the code, we create a new NpmScript class, and then run the process. The NpmScript class does a few things that I think folks will appreciate.

  1. It will wait for the Parcel process to complete running.
  2. It will parse the output for the webserver
  3. In the off chance we are not running the webserver, it will timeout and succeed
  4. We can write the console output of our NPM script to an Action
  5. It will throw if the process fails to start.
  6. The node process is a child process of our .NET Core application. When our app stops, so will all child processes.

After we run it, we get the following results:

running the process

Here is the Node process running in activity monitor.

running the process with activity monitor

When we close out the .NET Core application, we no longer see the node child processes.

running the process with activity monitor empty

The NpmScript Class

The following is the implementation of NpmScript, which uses a Process class to execute our NPM scripts.

public class NpmScript : IDisposable
{
    private readonly string scriptName;

    private static readonly Regex urls =
        new Regex(
            "(ht|f)tp(s?)\\:\\/\\/[0-9a-zA-Z]([-.\\w]*[0-9a-zA-Z])*(:(0-9)*)*(\\/?)([a-zA-Z0-9\\-\\.\\?\\,\\'\\/\\\\\\+&%\\$#_]*)?",
            RegexOptions.IgnoreCase | RegexOptions.Compiled);

    private Process process;
    public string Url { get; private set; }
    public bool HasServer => !string.IsNullOrEmpty(Url);

    public int ProcessId => process?.Id ?? 0;

    private readonly TaskCompletionSource<bool> signal = new TaskCompletionSource<bool>(false);

    /// <summary>
    /// Will execute a script name with default value of "dev"
    /// i.e. npm run [script name]
    /// </summary>
    /// <param name="scriptName"></param>
    public NpmScript(string scriptName = "dev")
    {
        this.scriptName = scriptName;
    }

    /// <summary>
    /// Will wait for npm to start up and retrieve the first url from the output.
    /// **Only use this to run the development server.**
    /// </summary>
    /// <param name="output"></param>
    /// <param name="timeout">In milliseconds</param>
    /// <returns></returns>
    public async Task RunAsync(Action<string> output = null, int timeout = 2000)
    {
        lock(signal) 
        {
            if (process == null)
            {
                var info = new ProcessStartInfo("npm")
                {
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    Arguments = $"run {scriptName}",
                    UseShellExecute = false,
                };

                process = Process.Start(info);
                process.EnableRaisingEvents = true;
                process.BeginOutputReadLine();
                process.BeginErrorReadLine();

                // Process the NPM output and attempt
                // to find a URL. This will stop processing
                // when it finds the first URL
                process.OutputDataReceived += (sender, eventArgs) =>
                {
                    output?.Invoke(eventArgs.Data);

                    if (!string.IsNullOrEmpty(eventArgs.Data) && string.IsNullOrEmpty(Url))
                    {
                        var results = urls.Matches(eventArgs.Data);

                        if (results.Any())
                        {
                            Url = results.First().Value;
                            signal.SetResult(true);
                        }
                    }
                };

                // Terrible things have happened
                // so we can stop waiting for the success
                // event to occur, because it ain't happening
                process.ErrorDataReceived += (sender, args) =>
                {
                    output?.Invoke(args.Data);

                    if (!signal.Task.IsCompleted)
                    {
                        signal.SetException(new Exception("npm web server failed to start"));
                    }
                };

                // set a timeout to wait for the process
                // to finish starting and find the Url. If it doesn't then we
                // assume that the user just ran a script
                var cancellationTokenSource = new CancellationTokenSource(timeout);
                cancellationTokenSource.Token.Register(() =>
                {
                    if (signal.Task.IsCompleted) 
                        return;

                    // we don't want to wait for a url anymore
                    Url = string.Empty;
                    signal.SetResult(true);
                }, false);
            }
        } 

        await signal.Task;
    }

    public void Dispose()
    {
        process?.Dispose();
    }
}

Conclusion

While you could run your frontend web server and .NET Core application as two separate processes, I believe this approach has some benefits that you should consider. First, NPM scripts run as a child process of your .NET Core application. More importantly, you can parse the output of your NPM scripts to create variables to use in your .NET Core applications (think ASP.NET Core). Finally, you can programmatically know whether the NPM scripts succeeded or failed, which can save you hours of debugging. If you’re a .NET Core developer, but still want to use the latest UI frameworks that the JavaScript community has to offer, I hope you consider using this approach. Also, feel free to modify the NpmScript class to meet your needs.

Download the project at GitHub.