ASP.NET Core provides a couple of great project templates to get you up and running quickly. However, the MPA (multiple-page application) is not getting a lot of attention. Packages are outdated and you can’t add new JS libraries using modern practices.

Starting out is easy though: either you choose the web template, which gives you an almost empty project, or you can opt for more boilerplate by choosing the mvc or webapp template.

Both have their (dis)advantages:

dotnet new web : this gives you… pretty much nothing. That’s not necessarily a bad thing if you’re up for configuring everything from scratch.

: this gives you… pretty much nothing. That’s not necessarily a bad thing if you’re up for configuring everything from scratch. dotnet new mvc (or webapp ): gives you an MPA right off the bat, but the front-end libraries are outdated and bundled with the project which doesn’t provide a lot of room for change.

( webapp results in a page-based project while mvc produces a Model-View-Controller project)

It can be challenging to get your front-end configured just the way you want, especially when you’re not starting out from the angular or react templates. If you want to do anything related to multiple pages/views or even if you want to use a SPA-framework other than the ones provided (such as Vue!) then this guide can help you set that up!

What we’re about to do

We’re going to set up an ASP.NET Core MVC project with custom front-end dependencies. It’ll be a multi-page app that is tailored to our needs. More specifically…

project with custom front-end dependencies. It’ll be a multi-page app that is tailored to our needs. More specifically… We’ll be making sure Bootstrap is a dependency of the project instead of a part of the project. So our project tree will no longer be cluttered with a bunch of minified JS and MAP files.

the project instead of a the project. So our project tree will no longer be cluttered with a bunch of minified JS and MAP files. We’ll be kicking out JQuery like it’s 2014! It’ll only be referenced from Bootstrap and ASP.NET’s form validation since both still depend on it.

The dependencies and build process will be set up using npm and webpack. Any JavaScript and CSS code that we write will be processed by this build task. We’ll move all .js and .css files into a separate src directory. Only the build artifact will belong in wwwroot .

Why we’re doing it

First and foremost, the MVC template is much too firm and inflexible. Adding new front-end libraries to the project is a bit of an undertaking and that shouldn’t be the case.

Adding a NuGet package to the project (thankfully) doesn’t result in a bunch of DDLs in the project tree. Neither should be the case when adding a library for the front-end. We also want control over the exact version numbers that are used, just like with NuGet packages.

Along the way we’ll get a better insight into how webpack can be used outside of a SPA!

Let’s get to it!

Creating the project

What you’ll need:

.NET Core SDK, I’m using 3.1

NodeJS with bundled npm (or yarn), I’m using 12.16.1

Optionally, an IDE. For now all you need is a functional command line!

Let’s create a new MVC project. Navigate to a new empty directory and enter the following:

$ dotnet new mvc

If you’re using git then now’s a good time to create a .gitignore file. And if someone at Microsoft is reading this, this file should be a part of the project template! :)

Launch the project with:

$ dotnet run

… and navigate to https://localhost:5001 to have a look.

This startup project can be found on Gitlab as the first version of NetCoreNpmWebpack.

Front-end dependencies

Next up, create a new directory called Client in the root of your MVC project. Create a file called package.json and place it in this new directory. Give it the following content:

You’ll want to change the values of the first five fields in case you’re applying these steps on an existing project.

Apart from the obvious fields we’re specifying:

dependencies : includes Bootstrap 4 and JQuery

: includes Bootstrap 4 and JQuery devDependencies : webpack and webpack-related loaders. We’ll need these to bundle our front-end code.

: webpack and webpack-related loaders. We’ll need these to bundle our front-end code. scripts : a single command that invokes the webpack bundling process

Go ahead and run the following command from within the Client directory:

$ npm install

A new node_modules directory will have popped up containing a massive amount of JS and CSS files. Consider this directory to be like the bin and obj directories (or better yet, the NuGet package store). Although it doesn’t contain any binaries, its content is still a number of dependencies that we refer to from within our own code.

… but we don’t have any code yet. Let’s add some by creating a src directory inside Client and adding both a js and a css directory at that location. Create a new file called site.js in the js directory and move site.css from wwwroot/css to Client/src/css . Like so:

Client/

├── package-lock.json

├── package.json

├── src

│ ├── css

│ │ └── site.css // moved from wwwroot/css

│ └── js

│ └── site.js // newly created

└── webpack.config.js

Cleanup is important so take your time remove wwwroot/css and wwwroot/js entirely.

In site.js we will write custom JavaScript code that’s relevant to the entire site. Additionally, we use this file to import all site-wide dependencies:

Lines 2 to 9 will import code from within node_modules . We’re importing Bootstrap’s Javascript and CSS as well as a couple of JQuery libaries that are used by ASP.NET Core’s validation scripts.

All of this is done using ECMAScript 6 modules, a modern approach to importing JS files from other JS files or even CSS files from JS files.

Now… how can we possibly get this code into a user’s browser? The file that we just created contains only 17 lines of code, but it imports a small list of libraries as well as some custom CSS code. Do we just throw node_modules at our users? Of course not! This directory is so large that we need to carefully filter, bundle and host only those parts that are needed at runtime.

… this is where webpack comes in!

Building the bundle

Create a new file called webpack.config.js and place it in the Client directory. Give it the following content:

Without going into too much detail, we’re specifying that a bundle should be created based on src/js/site.js and that this bundle should be called site.entry.js (the [name] tag is replaced with “site”).

webpack is smart enough to figure out what should be included in the bundle and it bases its decision-making on what we do in site.js . For example, if we use JQuery, then JQuery will be included in the bundle. Even CSS files such as those from Bootstrap will be included in the bundle if we choose to import them from within site.js . (In the rules section at the bottom we specify how those non-JS files are handled.)

Ok, let’s build this using the following command from within the Client directory:

$ npm run build

Basically, this executes the webpack command as can be seen in the scripts section of package.json above.

The build results in two new files:

wwwroot/dist/

|-- site.entry.js

`-- site.entry.js.map

Now we can include those from within our HTML. Open Views/Shared/_Layout.cshtml and replace all script and link tags (even those at the bottom!) with a single line:

type="module" indicates that this JS file contains “ECMAScript 6 modules” and defer (which is actually the default for such modules) indicates that execution of this file should be delayed until the page is fully loaded. That’s a modern alternative to putting all script tags at the bottom of a document in the hope that the document is fully downloaded before the scripts start scanning the DOM.

Time to delete the last bit of clutter from our project tree:

$ rm -rf wwwroot/lib

If you’re as obsessed with useless comments as I am then now’s the time to remove the top comment in site.css . Whatever minification reference is being made there, we’re not going down that path anyway.

And if you’re using git then make sure to add wwwroot/dist/ to your .gitignore .

Alright, time to check it out!

$ dotnet run

Open https://localhost:5001 in a browser and check the browser console (F12). You should see the message “The ‘site’ bundle has been loaded!”.

How cool is that? All of the front-end’s dependencies are now downloaded, built and bundled dynamically. The build system is extensible and all dependencies are easily managed and replaceable if needed!

The project so far can be found on Gitlab as version 2 of NetCoreNpmWebpack.

What’s next?

We’re not quite finished. There are two important things on our TODO list:

Performance! Try switching quickly between different pages in the SPA. You’ll see a delay before Bootstrap’s CSS gets applied. It’s caused by a large amount of CSS getting plugged into the page at runtime. This is the most urgent thing to fix, user experience is just too important!

Try switching quickly between different pages in the SPA. You’ll see a delay before Bootstrap’s CSS gets applied. It’s caused by a large amount of CSS getting plugged into the page at runtime. This is the most urgent thing to fix, user experience is just too important! Building the project as a whole. At this point building the .NET project doesn’t trigger a rebuild for the front-end. How annoying! :)

In part 2 we’ll be tackling the performance issue and streamlining the build process.