Last Updated: 12/30/2015

I recently embarked on a Single Page Application (SPA) project in which React was to be used with ASP.NET Core 1.0 (RC1) and Webpack. Due to some single sign-on security requirements, the decision was made to initially render the application from an ASP.NET MVC 6 view. Barring this constraint, the more traditional index.html file would have easily sufficed as the application’s shell. What did this MVC view buy me? It allowed me to leverage my C# skillset to satisfy the security requirements in an MVC controller. Specifically, I needed to analyze some HTTP request header values.

The requirement described above undoubtedly complicated things, but there was another conundrum: cache busting. A good cache busting strategy must be mindful of two extremities in particular: the development workflow and the production environment. During development, a unique collection of file names should be produced for each Webpack build. If clearing the browser’s cache immediately following a build of client-side assets has become commonplace in your development workflow, carve out some time to identify the underlying problem. When deploying to production, the files which changed since the last Webpack build should be assigned new, unique file names. After all, the application’s users shouldn’t be inconvenienced due to poor planning on part of the development team. Give the customer one less reason to consider doing business with the competitor.

Making Sense of Webpack Hash Keys

Webpack is capable of yielding file names which are supportive of the cache busting obligation. This is made possible by a couple of hash key placeholders which can be used in the output section of the Webpack configuration file. Those placeholders are:

  1. [hash] – A generated value which is unique to a build.
  2. [chunkhash] – A generated value which is unique to each chunk in a build.

These hash keys are made visible in the Webpack build output (see screenshot below). For example, the hash key used in the vendor.c7a466d957209719c8d9.js file name was produced with the chunkhash placeholder. The 67f2bdb1268bed71d35e hash key provided near the top of the build output represents the hash placeholder value.

webpack_build_output

This walkthrough will utilize chunkhash; however, there’s additional work to be done outside of this placeholder. Namely, the values assigned to the chunkhash placeholder must be extracted. Without these extracted bits, it’s impossible to reference the requisite client-side assets which were run through the Webpack build.

After a bit of research, I stumbled upon an npm module called html-webpack-plugin. This utility seemed promising, as it generates an index.html file with ease. My hope was that it could be adapted to a C# Razor view. Hours of hacking passed, and while I came close, I was unable to get this working to my satisfaction. Mission aborted, and onto the next option for solving the problem.

Striking Gold with the Webpack Assets Plugin

The assets-webpack-plugin was the next plugin I tried, and this did the trick. In a nutshell, this plugin generates a JSON file containing the generated file/chunk names. Unless configured otherwise, the JSON file’s default name is webpack-assets.json. I’ve changed the file name slightly, to webpack.assets.json, for demonstration purposes. Assume that my Webpack configuration file will produce two chunks: app and vendor. The resulting webpack.assets.json file will look as follows:


{
"vendor": {
"js": "vendor.c7a466d957209719c8d9.js"
},
"app": {
"js": "app.eaa3ef606862a9d2be7d.js"
}
}

The supporting ES6-based Webpack configuration file could look as follows:


'use strict';
const AssetsPlugin = require('assets-webpack-plugin');
const CleanPlugin = require('clean-webpack-plugin');
const path = require('path');
const pkg = require('./package');
const webpack = require('webpack');
const BUILD_DIRECTORY = 'build';
const BUILD_DROP_PATH = path.resolve(__dirname, BUILD_DIRECTORY);
const CHUNK_FILE_NAME = '[name].[chunkhash].js';
const WEB_ROOT = path.resolve(__dirname, 'wwwroot');
let config = {
context: WEB_ROOT,
entry: {
vendor: Object.keys(pkg.dependencies),
app: './app'
},
module: {
loaders: [
{
test: /\.jsx?$/,
loader: 'babel',
include: WEB_ROOT
}
]
},
output: {
chunkFilename: CHUNK_FILE_NAME,
filename: CHUNK_FILE_NAME,
libraryTarget: 'var',
path: BUILD_DROP_PATH
},
plugins: [
new AssetsPlugin({
filename: 'webpack.assets.json',
path: BUILD_DROP_PATH,
prettyPrint: true
}),
new CleanPlugin(BUILD_DIRECTORY),
new webpack.optimize.CommonsChunkPlugin('vendor', CHUNK_FILE_NAME),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
output: {
comments: false
}
})
],
resolve: {
extensions: ['', '.js', '.json', '.jsx']
}
};
if (process.env.NODE_ENV === 'development') {
config.cache = true;
config.devtool = 'eval';
config.watch = true;
}
module.exports = config;

Direct your attention to line 41 in the gist provided above. This is where the assets-webpack-plugin configuration is defined. In review, the plugin configuration section is accomplishing the following three tasks:

  1. naming the generated file webpack.assets.json
  2. copying the generated JSON file to the build folder
  3. enabling pretty printing/formatting of the JSON

Parsing the JSON File

Now that the necessary client-side assets have been dropped to the build folder, the C# code must read webpack.assets.json. The unique JavaScript file names must be retrieved for injection into the MVC Razor view. With the assistance of the JSON.NET NuGet package, the following code does the job with ease:


public static JObject GetWebpackAssetsJson(string applicationBasePath)
{
JObject webpackAssetsJson = null;
string packageJsonFilePath = $"{applicationBasePath}\\{"package.json"}";
using (StreamReader packageJsonFile = File.OpenText(packageJsonFilePath))
{
using (JsonTextReader packageJsonReader = new JsonTextReader(packageJsonFile))
{
JObject packageJson = (JObject)JToken.ReadFrom(packageJsonReader);
JObject webpackConfigJson = (JObject)packageJson["customConfig"]["webpackConfig"];
string webpackAssetsFileName = webpackConfigJson["assetsFileName"].Value<string>();
string webpackBuildDirectory = webpackConfigJson["buildDirectory"].Value<string>();
string webpackAssetsFilePath = $"{applicationBasePath}\\{webpackBuildDirectory}\\{webpackAssetsFileName}";
using (StreamReader webpackAssetsFile = File.OpenText(webpackAssetsFilePath))
{
using (JsonTextReader webpackAssetsReader = new JsonTextReader(webpackAssetsFile))
{
webpackAssetsJson = (JObject)JToken.ReadFrom(webpackAssetsReader);
}
}
}
}
return webpackAssetsJson;
}

An object of type JObject is returned. With that object in hand, the MVC controller is equipped to fetch the desired file names and inject them into the ViewBag for retrieval in the view.

There was one challenge in particular that’s worth noting in regards to the helper method provided above. The helper method expects a parameter value representing the fully-qualified path to the project’s root folder. This value allows the method to locate the package.json file and the generated build folder. Since the application is compiled against .NET Core 1.0, we only have a subset of the features provided in the full .NET Framework (e.g., 4.6). In short, the old way of doing this (AppDomain.CurrentDomain.BaseDirectory) isn’t supported in .NET Core. The new approach to retrieving this desired path involves using the IApplicationEnvironment interface from the Microsoft.Extensions.PlatformAbstractions namespace. ASP.NET Core 1.0 is adorned with dependency injection, so constructor injection can be used to handle this:


private string _applicationBasePath = null;
public HomeController(IApplicationEnvironment env) {
_applicationBasePath = env.ApplicationBasePath;
}

Wiring up the Generated Assets to the MVC 6 View

The Index action method corresponding to the MVC view looks as follows:


public IActionResult Index()
{
const string JAVASCRIPT_KEY = "js";
JObject json = WebpackHelper.GetWebpackAssetsJson(_applicationBasePath);
ViewBag.VendorScripts = json.SelectToken("vendor").Value<string>(JAVASCRIPT_KEY);
ViewBag.AppScripts = json.SelectToken("app").Value<string>(JAVASCRIPT_KEY);
return View();
}

Line 5 invokes the helper method described in the previous section. Lines 6 and 7 retrieve specific file names from the JObject and inject them into the view via the ViewBag.

In the view, the two script tags appearing immediately before the closing body tag look like this:


<script src="@Url.Content(ViewBag.VendorScripts)"></script>
<script src="@Url.Content(ViewBag.AppScripts)"></script>

view raw

Index.cshtml

hosted with ❤ by GitHub

Since we’re leveraging ASP.NET Core 1.0 and running under IIS, there’s something worth noting here. By default, all static assets in the application are served from the wwwroot folder. Without any additional configuration, an attempt to serve files outside of that folder will fail. What additional configuration is required to serve files from the generated build folder? A hosting.json file must be created at the project root, and its contents should be nothing more than the following:


{
"webroot": "build"
}

view raw

hosting.json

hosted with ❤ by GitHub

Wrapping Up

Attempting to learn Webpack can be daunting, especially since the official documentation leaves much to be desired. Couple that with the moving target that is ASP.NET Core 1.0, and it becomes quite the chore to get the two working together. Introduce the cache busting requirement for client-side assets, and you’re involved in a wild goose chase trying to get all the moving pieces stitched together. The good news is that all the hard work leads to a very robust solution.

Hopefully I’ve saved someone a few headaches with this blog post. The complete sample application described in this post can be found in this Git repository.

15 Comments »

  1. Nice article Scott. I am glad to see that more people using webpack with asp.net.

    But let me ask you if you use a regular razor view why you just don’t let asp do the cache busting. I mean doing something like this

    http://~/js/vendor.min.js

    During development you can use the webpack-dev-server and serve all files from memory to have a live reload experience of course.

    Like

    • My assumption is that you’re referring to the ASP.NET bundling and minification available in the Microsoft.AspNet.Web.Optimization NuGet package. That particular approach is deprecated for ASP.NET Core. See this article for more info: http://www.jeffreyfritz.com/2015/05/where-did-my-asp-net-bundles-go-in-asp-net-5/.

      If you were to install this NuGet package in an ASP.NET Core project which targets both .NET Framework and .NET Core, you’d see that the NuGet package is only installed for .NET Framework (e.g., DNX 4.5.1). I suspect this is due to the fact that .NET Core lacks System.Web support, and the NuGet package has a dependency on System.Web.Optimization.

      If you’re curious, here’s what you’d see in the project.json file:

      "frameworks": {
      "dnx451": {
      "dependencies": {
      "Microsoft.AspNet.Web.Optimization": "1.1.3"
      }
      },
      "dnxcore50": { }
      },

      Like

  2. Sorry my mistake my comment didn’t show as I expected.

    No I didn’t mean to use the old and well know bundling that comes with full asp.net. Now if you create a default mvc application in the Layout.cshtml file you will see there is a reference to the site.js file for production environment and has an addition attribute that looks like

    asp-append-version=”true”

    So there is a cache busting mechanism for the new asp.net. At least it exists in version RC update 1 and I would be very surprise if it will be removed.

    Like

  3. Now that you mention it, I have seen what you’re talking about. This would make a great follow-up blog post to compare that approach to what I wrote about here. Looks like I have some more research to do. I’ll add that to my to-do list.

    Thank you for the comments. This is how we all learn. Keep them coming!

    Like

  4. Hey Scott,
    Thanks SO much for taking the time to share your experiences and solutions. I’ve just started drinking from this fire hose and your blog has saved me a ton of time and grief.

    Liked by 1 person

  5. I’d probably do this quite a bit differently. The way you’re doing it here, assuming I’m not missing something obvious, you’re actually reading and parsing the webpack.assets.json file on every single request. You ought to load and parse this file during configuration instead, then make it available through e.g. dependency injection with singleton life-cycle (or some other means).

    That naturally means that, in production, you’d have to restart the webserver whenever your static files change. But since those changes may come along with changes to your asp.net application, the webserver would probably have to be restarted anyways.

    Like

Leave a comment