ASP.NET used to have a nice Webpack integration with the officially supported Microsoft.AspNetCore.SpaServices Nuget package. In August 2019 Microsoft announced that it would be deprecating this package and suggested a new integration mechanism with Microsoft.AspNetCore.SpaServices.Extensions.

Since I use this with Vue.js and the new integration mechanism doesn’t offer the same functionality, I’ll share how I managed to get up and running again in this post.

The problem and what Microsoft suggests

In this announcement, Microsoft made the official statement:

[Announcement] Obsoleting Microsoft.AspNetCore.SpaServices and Microsoft.AspNetCore.NodeServices

As you can see, there’s still an active discussion after a year, mainly because Microsoft suggests to migrate to a new integration mechanism that lacks a lot of functionality.

A working solution

Let me start by stating this isn’t the ideal solution, but it works (Hot Module Replacement too).

It consists of a few steps:

  1. Start the development server from Startup.cs
  2. Add NPM scripts that get called by .NET Core
  3. Update Webpack configuration with webpack-dev-server
  4. Configure publishing & production builds

In the rest of the article I’m assuming you have a basic working setup and knowledge of the technologies we’re working with. I’ve replaced irrelevant code with “…” to increate readability.

1. Start the development server from Startup.cs

This step is pretty straightforward. update Startup.cs to include this:

using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer;

...

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
...
            if (env.IsDevelopment())
            {
                // run webpack-dev-server for development builds using build:hotdev NPM script
                // NPM commands are defined in package.json
                app.Map("/dist", innerApp =>
                {
                    innerApp.UseSpa(spa =>
                    {
                        spa.Options.SourcePath = ".";
                        spa.UseReactDevelopmentServer(npmScript: "build:hotdev");
                    });
                });
            }
...

What this code does, is call the NPM script buid:hotdev. If you’re using Vue.js, don’t worry about the React reference. It’s still just a piece of code that calls the NPM script.

2. Add NPM scripts that get called by .NET Core

This should look familiar:

"scripts": {
  // ...
  "build:hotdev": "npx webpack-dev-server --open --hot --mode development",
  "build:live": "npx webpack --display-error-details --mode production --env.prod"
  // ...
}

3. Update Webpack configuration with webpack-dev-server

Install the dependencies:

npm install --save-dev webpack-dev-server html-webpack-plugin html-webpack-harddisk-plugin

And update your Webpack configuration to look something like this:

// webpack.config.js
// ...
const HtmlWebpackPlugin = require('html-webpack-plugin')
var HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin')

module.exports = (env) => {
    const isDevBuild = !(env && env.prod)

    if (isDevBuild) {
        // don't remove - next line tricks Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer into executing
        console.log('Starting the development server...\n')
    } else {
        console.log('Running production build...\n')
    }
    // ...
    return [{
        // ...
        devServer: {
            contentBase: path.join(__dirname, './wwwroot/dist'),
            compress: true,
            overlay: true,
            port: 5001,
            proxy: {
                '/': 'http://localhost:5000'
            }
        },
        // ...
        plugins: [
            // ...
            new HtmlWebpackPlugin({
                inject: false,
                template: 'Views/_HtmlWebpackTemplate.cshtml',
                filename: '../../Views/Home/Index.cshtml',
                alwaysWriteToDisk: true,
                minify: false
            }),
            new HtmlWebpackHarddiskPlugin({
                // Enhances html-webpack-plugin functionality by adding the {alwaysWriteToDisk: true|false} option.
                outputPath: path.resolve(__dirname, 'wwwroot/dist/')
            })
        ]
    }]
}

Important: pay close attention to console.log('Starting the development server...\n'). This is the main hack that makes this method work (source).

This code starts webpack-dev-server on port 5001. If you’re not using the default port (5000), please update this section. It also makes sure your cshtml templates get updated with the assets injected by the development server.

To make sure the cached result gets updated and recognised by .NET Core,html-webpack-harddisk-plugin is used. The exact configuration of the templates is out of the scope of this writeup, but feel free to get in touch if you need help!

4. Configure publishing & production builds

webpack-dev-server is nice for development, but what about production builds? The .csproj file needs to be modified for this.

YourProject.csproj:

<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
    <!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Exec</span> <span class="token attr-name">Command</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>npm install<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Exec</span> <span class="token attr-name">Command</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>npm run build:live<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>

<span class="token comment">&lt;!-- Include the newly-built files in the publish output --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ItemGroup</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>DistFiles</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>wwwroot\dist\**<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ResolvedFileToPublish</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>@(DistFiles-><span class="token punctuation">'</span>%(FullPath)<span class="token punctuation">'</span>)<span class="token punctuation">"</span></span> <span class="token attr-name">Exclude</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>@(ResolvedFileToPublish)<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RelativePath</span><span class="token punctuation">></span></span>%(DistFiles.Identity)<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>RelativePath</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>CopyToPublishDirectory</span><span class="token punctuation">></span></span>PreserveNewest<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>CopyToPublishDirectory</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ResolvedFileToPublish</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ItemGroup</span><span class="token punctuation">></span></span>

</Target>

:hexoPostRenderEscape–>

Workaround: stopping webpack-dev-server

Unfortunately, the console.log() hack in step 3. is a one-way process, meaning the webpack-dev-server will keep running until you close it manually. This means you can have conflicting servers running at the same time.

To resolve this, I’ve added a small script that gets run before the Build step. This is compatible with both Windows and Mac/Linux setups:

<Target Name="KillRunningNodeServer" BeforeTargets="Build">
  <Message Importance="high" Text="Killing old Node server..." />
  <Exec Command="taskkill /f /im node.exe" ContinueOnError="true" Condition="'$(OS)' == 'Windows_NT'" />
  <Exec Command="killall node" ContinueOnError="true" Condition="'$(OS)' == 'Unix'" />
</Target>

Hopefull this will save you a couple of hours of puzzling and testing!


Sources