image

Update: ASP.NET Core 2.1 and webpack 4 are released. I’ve written a new post, Lean ASP.NET Core 2.1 – manually setup a Razor Pages project with Bootstrap, NPM and webpack.

A while ago I wrote about building an ASP.NET Core MVC app with NPM and webpack.  Since then, ASP.NET Core 2.0 was released. Compared with ASP.NET Core 1.x, things have become easier, especially configuration. Quite a few settings that had to be added manually before are now the default. This post is an update of the previous post that is compatible with ASP.NET Core 2.0.

Contents:

A little bit of knowledge of ASP.NET MVC and Visual Studio is assumed. If you already know how to create an ASP.NET Core MVC app from scratch or if you’re mainly interested in the NPM/webpack thing you might skip the first paragraphs and jump right to step 3.

The source code of the project is at https://github.com/martijnboland/MinimalAspNetCore.

Starting from scratch

The goal of this exercise is to start with the empty ASP.NET Core template and add just enough to have a basic ASP.NET Core MVC app with a home page that is styled with Bootstrap and can be deployed to production environments.

Let’s start with the empty template:

image_thumb[2]

resulting in the following project:

image

Step 1: Add configuration files and configure ASP.NET

We’ll add the files appsettings.json and appsettings.Development.json to the root of the project. These files contain our logging configuration, together with other configuration options. See https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration for an overview of ASP.NET Core configuration and https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging for ASP.NET Core logging.
The file Startup.cs is the place where everything is wired together:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        app.UseStaticFiles();
        app.UseMvcWithDefaultRoute();
    }
}

At this point we have our base infrastructure in place for ASP.NET Core MVC pages. However, running the application, now results in a 404 not found error. What we need are a controller, some views and a layout.

As it turns out, this looks remarkably similar to the ASP.NET Core Web API template. An nice alternative is to take the Web API template as the starting point, and add app.UseStaticFiles() to Startup.cs.

Step 2: Add controller, views and a layout

image This is straightforward ASP.NET MVC, see this page for an introduction:

  • Create folder ‘Controllers’ and add a file HomeController.cs with actions Index() and Error()
  • Create folder ‘Views’ and add _ViewStart.cshtml (Add New Item –> MVC View Start Page) and _ViewImports.cshtml (Add New Item –> MVC View Imports Page)
  • In the Views folder create the folders ‘Home’ and ‘Shared’
  • Add ‘Index.cshtml’ to the ‘Views/Home’ folder and _Layout.cshtml and Error.cshtml to the ‘Views/Shared’ folder

The layout page (_Layout.cshtml) already has some bootstrap-specific markup:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"]</title>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-brand">MinimalAspNetCore</a>
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
            </ul>
        </div>
    </div>
</nav>
<div class="container body-content">
    @RenderBody()
</div>
</body>
</html>

This results in a working page without styles:

image

To make this page a bit prettier, we’re adding Bootstrap (and jQuery because the Bootstrap JavaScript plugins depend on jQuery).

Step 3: Add client-side libs from NPM

Bootstrap and jQuery are installed from the NPM repository. NPM is the package manager of Node.js. We have to make sure that a reasonably recent version of Node.js (version 6 or higher will do) is installed on our system and that it is on our path so we can run the node and npm commands from a console:

image

NPM requires a package.json file in the root of the project, so we’re adding that (Add New Item –> npm Configuration file, or from the command line in the root folder with ‘npm init’). After that, we’ll install our client libs with the following command (from the application root folder):

npm install –save jquery bootstrap

The libraries are installed, but how can we use these? The files are installed in the ‘node_modules’ folder, but we cannot simply reference those in our layout page, because scripts and css files need to be in the ‘wwwroot’ folder. What we need is a tool that bundles all required client-side scripts and styles and copies the bundle to the ‘wwwroot’ folder.

Step 4: Bundle with webpack

Webpack is becoming the standard for bundling and minification of client-side scripts, but it can do much, much more. Many people complain that setting up webpack is complex, but I honestly think that it is not. Let’s continue with our project.

For simplicity we’re going to create a single bundle that contains both our scripts and also our css styles. This way we only have to reference a single file in our layout page. The following code goes into the _Layout.cshtml file just before the </body> tag (in Staging and Production environments, a version is added for cache busting):

<environment names="Development">
    <script src="~/dist/main.js"></script>
</environment>
<environment names="Staging,Production">
    <script src="~/dist/main.js" asp-append-version="true"></script>
</environment>

Thus our goal becomes to create a single bundle ‘main.js’ that goes into the ‘wwwroot/dist’ folder.

Add webpack and some loaders to our project:

npm install --save-dev webpack style-loader css-loader url-loader file-loader

After that we can add the webpack configuration file to the root of our project (webpack.config.js):

var path = require('path');
var webpack = require('webpack');

module.exports = function (env) {

  env = env || {};
  var isProd = env.NODE_ENV === 'production';

  // Setup base config for all environments
  var config = {
    entry: {
      main: './Client/js/main'
    },
    output: {
      path: path.join(__dirname, 'wwwroot/dist'),
      filename: '[name].js'
    },
    devtool: 'eval-source-map',
    resolve: {
      extensions: ['.ts', '.tsx', '.js', '.jsx']
    },
    plugins: [
      new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery' })
    ],
    module: {
      rules: [
        { test: /\.css?$/, use: ['style-loader', 'css-loader'] },
        { test: /\.(png|jpg|jpeg|gif|svg)$/, use: 'url-loader?limit=25000' },
        { test: /\.(png|woff|woff2|eot|ttf|svg)(\?|$)/, use: 'url-loader?limit=100000' }
      ]
    }
  }

  // Alter config for prod environment
  if (isProd) {
    config.devtool = 'source-map';
    config.plugins = config.plugins.concat([
      new webpack.optimize.UglifyJsPlugin({
        sourceMap: true
      })
    ]);
  }

  return config;
};

The essential parts in the webpack configuration are the ‘entry’ and the ‘output’ fields. We’re basically saying: ‘analyze the entry file (‘Client/js/main’), resolve all references from there, put everything together in a single file and write that to the ‘wwwroot/dist’ folder with the name of the entry as file name (‘main.js’).

For the other config options, please check the webpack configuration reference and the loaders documentation to see what those are all about. It goes beyond the scope of this post to explain that all in detail.

imageYou might already have noticed that the entry file as defined in the webpack config does not yet exist. Let’s create one in the ‘Client/js’ folder as main.js (together with ‘Client/css/site.css’ for some custom styles). The file ‘Client/js/main.js’ contains:

import '../css/site.css';
import 'bootstrap/dist/css/bootstrap.css'
import 'jquery';
import 'bootstrap';

There is no code in this file apart from some ES6-style imports. However, exactly these imports are used by webpack to determine what goes into the bundle. Also notice that our css file and the bootstrap css file are imported just as if they were JavaScript files.

Now we can create the bundle, but how do we instruct webpack to do that? The easiest way to do it is by adding commands to the scripts section of the NPM package.json file:

"scripts": {
  "start": "webpack --config webpack.config.js --watch --progress --profile",
  "build": "webpack --config webpack.config.js --progress --profile",
  "prod": "webpack --env.NODE_ENV=production --config webpack.config.js --progress"
}

To create a development bundle enter:

npm run build

To create a minified production bundle:

npm run prod

During development, it’s convenient that bundles are updated when something changes. This can be accomplished with:

npm start

After having created the bundle with one of the above commands we see this when we run the application:

image

Looks like Bootstrap doesn’t it? 🙂

Note: you can fully integrate the NPM commands in your Visual Studio workflow with the Task Runner Explorer and the NPM task runner extension.

Step 5: Publish

So there we have it. An ASP.NET Core MVC web application with NPM and webpack. Finally, we need something that creates our production bundle during publish. This can simply be done by just adding a few lines to the .csproj file:

<Target Name="BuildClientAssets" AfterTargets="ComputeFilesToPublish">
  <Exec Command="npm install" />
  <Exec Command="npm run prod" />

  <!-- Include the newly-built files in the publish output -->
  <ItemGroup>
    <DistFiles Include="wwwroot\dist\**" />
    <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
      <RelativePath>%(DistFiles.Identity)</RelativePath>
      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
    </ResolvedFileToPublish>
  </ItemGroup>

</Target>

After having published the production bundle is in the wwwroot/dist/ folder of the published web app.

And now?

Like this title already says: this is the minimal app. The source code is at https://github.com/martijnboland/MinimalAspNetCore. There is some room for improvement. The first thing to do is to extract the css from the production bundle and move that to its own file to prevent the flicker when loading the app. Also, when the app grows it’s probably wise to extract the shared vendor libraries into its own bundle. But this is probably something for a follow-up post.

Building the minimal ASP.NET Core MVC app with NPM and webpack – the 2.0 edition
Tagged on:         

15 thoughts on “Building the minimal ASP.NET Core MVC app with NPM and webpack – the 2.0 edition

  • December 4, 2017 at 2:54 pm
    Permalink

    This one helped me a lot today, thanks Martijn!

  • December 10, 2017 at 7:59 pm
    Permalink

    thanks for the quick setup for ASP.NET Core 2.0!! I have started learning C# (coming from a web developer background) and this really helps me link my existing knowledge of NPM with .NET Core

  • December 10, 2017 at 9:35 pm
    Permalink

    Thanks, glad I could help

  • January 22, 2018 at 4:28 pm
    Permalink

    Great article, I have a question about “Step 4: Bundle with webpack”, in the past I add cache busting in webpack by including [chunkhash] in the name. From your example, it’s using “asp-append-version=true”, are they the same concept? will it generate the same or different Hash IF the file content has NOT changed? Thanks for sharing.

  • February 19, 2018 at 6:01 pm
    Permalink

    One question Martijn, why should one separate vendor and application scripts, instead bundling all into one bundle?

  • February 20, 2018 at 3:53 pm
    Permalink

    Two reasons: a vendor bundle probably changes less often than the app code, so you can benefit from browser caching and webpack rebuilds are faster when only the app bundle needs a rebuild (at least, this used to be, don’t know if that’s still the case).

  • February 20, 2018 at 4:46 pm
    Permalink

    Yes, that’s what I thought of too, thanks for your confirmation 😉 Keep on the great work!

  • April 21, 2018 at 5:51 am
    Permalink

    Amazing article! Just one thing, I believe target property should be “node” for webpack config. Because it defaults to web, and creates errors while trying to resolve modules references fs etc.
    https://webpack.js.org/concepts/targets/

    Also people who are using Bootstrap 4 should consider install and import “popper.js” in main.js.
    Rest seems to be working fine even with webpack 4 with some minor warnings.

    Thanks!

  • April 22, 2018 at 8:26 pm
    Permalink

    I don’t think the target property should be ‘node’. And yes, that should create errors when using fs etc. in the browser because those are not available anyway in the browser.

  • April 26, 2018 at 9:26 pm
    Permalink

    This does not work with bootstrap 4.1 for me. Any suggestions?

  • April 27, 2018 at 5:48 pm
    Permalink

    ok i had wrog popper.js version i tried popper.js 1.14.3 and work fine!

  • May 17, 2018 at 2:47 pm
    Permalink

    Great article! However when I try to use jQuery in a view, I always get error “Uncaught ReferenceError: $ is not defined”. What am I missing?

  • May 17, 2018 at 6:37 pm
    Permalink

    When you want to use the global $ variabele, you have to expose it with the expose-loader (https://github.com/webpack-contrib/expose-loader). Run `npm install –save-dev expose-loader` and add the following rule to webpack.config.js in the module.rules array:

    {
    test: require.resolve(‘jquery’),
    use: [{
    loader: ‘expose-loader’,
    options: ‘$’
    }]
    }

    Also, make sure that any reference to $ happens after your main.js has been loaded.

  • May 18, 2018 at 6:46 am
    Permalink

    This is it! Thank you!

Leave a Reply

Your email address will not be published. Required fields are marked *