This post is part of a multi-post series:
- Lean ASP.NET Core 2.1 – manually setup a Razor Pages project with Bootstrap, NPM and webpack (this post)
- Lean ASP.NET Core 2.1 – add a React application to an existing Razor Pages application
- Lean ASP NET Core 2.1 – React forms, validation and Web API integration
In the past, I have written about setting up a minimal ASP.NET Core MVC application with NPM and webpack (ASP.NET Core 1.1 version, ASP.NET Core 2.0 version). These posts were mainly inspired by some unfortunate choices that had been made in the default Visual Studio project templates.
I tried to show an alternative way to manually setup an ASP.NET MVC project and use ‘modern’ commonly-used tools like webpack and NPM to create a simple ASP.NET MVC application with jQuery and Bootstrap for some client-side juice.
But isn’t this modern web development where everything changes all the time?
Yes :-). Fast-forward a few months and we’re in quite a different situation:
- ASP.NET Core 2.1 has been released with a lot of improvements and new features (for example, Razor Pages);
- The ASP.NET Core templates are improved significantly. Gone are Bower and things like bundleconfig.json. The simple templates just include the actual .js and .css files without requiring a package manager and the more complex SPA templates (angular, react) use NPM and webpack;
- Webpack 4 is released with more carefully chosen default settings, so less configuration is required;
So ditch the manual setup and pick a template?
Yes and no. In more situations than before, the default templates will now probably suffice, but on the other hand, I feel there’s something missing between the simple templates and the complex SPA templates.
Often, I want to start with a clean simple template but I also want to use NPM and webpack. There’s no template that covers this middle ground and the SPA templates are a bit overkill when you don’t have a SPA.
However, in this post I’ll show how easy it is to cover this middle ground with a manual setup. Starting with a simple template and manually add everything else step by step. I’m calling this a ‘lean’ application because we’re only adding the minimal required parts, but with keeping all options open for future extensions.
Building a ‘lean’ ASP.NET Core 2.1 application
Just like in the previous posts, we’re building a basic ASP.NET Core app with a home page that is styled with Bootstrap from NPM, built with webpack and can be deployed to production environments. For the impatient: get the source code at https://github.com/martijnboland/LeanAspNetCore ;-).
Prerequisites:
- .NET Core 2.1 SDK
- Node.JS 6.9.0 or higher
- Your favorite code editor, Visual Studio is not required but can also be used
- Some basic knowledge about ASP.NET, Razor, JavaScript
Since ‘lean’ is our objective we’re keeping it simple and don’t use our heavyweight friend Visual Studio but stick to the command line and a text editor. Everything works fine on Mac OS too.
First steps
- Create a new empty ASP.NET Core project based on the webapi template. This project template serves as a nice clean starting point that has appSettings.json etc. setup but not too much garbage that needs to be deleted anyway:
mkdir LeanAspNetCore cd LeanAspNetCore dotnet new webapi
- Create a Pages folder in the folder where new project is created. Add the first Razor Page, Index.cshtml to the Pages folder:
@page <h1>Index</h1>
- Locate the Properties/launchSettings.json file and set the values of both launchUrl attributes to “”:
- Run the application:
dotnet run
- Open your browser and browse to https://localhost:5001. You should see the following:
It works but it does look a bit basic don’t you think?
Styles, scripts and bundling
To make things look a bit less basic we’ll just add some Bootstrap styles. But also, it’s time to setup our client-side build infrastructure.
- Create a ClientApp folder and install libraries there with NPM. The folder is named ClientApp simply because it’s called that way in the official SPA templates, so it will look already familiar for some of you.
mkdir ClientApp cd ClientApp npm init -y npm install --save jquery popper.js bootstrap npm install --save-dev webpack webpack-cli style-loader css-loader
With the above commands we created a package.json file, installed bootstrap with its dependencies (jquery and popper.js) and the build libraries (webpack with style and css loaders).
- Add a css file in /ClientApp/styles/style.css. Here, we import the bootstrap css and add our own custom styles:
@import '~bootstrap/dist/css/bootstrap.css'; body { background-color: #cdf; }
Note: the ~ in the import indicates that webpack should look in the node_modules folder.
- Create an entry point for the client-side bundle. This is the JavaScript file that imports all dependencies. In our case it’s located in /ClientApp/js/main.js:
import '../styles/style.css'; import $ from 'jquery'; import 'popper.js'; import 'bootstrap';
Note that the css file is also imported here.
- Add a webpack configuration file (/ClientApp/webpack.config.js)
const path = require('path'); module.exports = (env = {}, argv = {}) => { const config = { mode: argv.mode || 'development', // we default to development when no 'mode' arg is passed entry: { main: './js/main.js' }, output: { filename: '[name].js', path: path.resolve(__dirname, '../wwwroot/dist'), publicPath: "/dist/" }, module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] } ] } } return config; };
See the webpack documentation for the configuration options. The configuration file above means: ‘start building from the ./js/main.js file, write the results in the ../wwwroot/dist folder and process css files via the css-loader and style-loader’.
Webpack 4 has a new parameter: ‘mode’. The modes are ‘development’ and ‘production’ and webpack uses sensible default settings for the modes so you don’t have to configure these yourself anymore. See this post for more information.
- Add a script command to package.json that executes the webpack build:
- Build the client bundle (still from ClientApp folder):
npm run build
After building, there is a single file ‘main.js’ in the /wwwroot/dist folder.
- All we need to do now is to create a Razor partial _Header.cshtml file with a Bootstrap navigation bar and _Layout.cshtml file that includes the header file and add a script link to ~/dist/main.js:
Noticed the environment tag helpers in the image above? We just include the main.js script file in the development environment but for the other environments, a version is appended to the file for cache busting.
The layout file is set as the default layout for all pages via _ViewStart.cshtml in the Pages folder. To enable the tag helpers we must add a _ViewImports.cshtml file to the Pages folder:@namespace LeanAspNetCore.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
For more information about Razor layouts, _ViewStart.cshtml and ViewImports.html, visit this page.
- Add the StaticFiles middleware to Startup.cs just before app.UseMvc() so we can serve the js and other static files:
app.UseStaticFiles();
- Go to the project root folder in the command line and execute:
dotnet run
When browsing to https://localhost:5001 you should now see a nice styled page:
That’s it! With a few steps we added Bootstrap from NPM and we’re able to include it in our application with the help of webpack.
Automate all the things
Having to execute ‘npm run build’ every time you want to see the results of a small change is getting tedious pretty fast . Luckily, ASP.NET Core already contains webpack development middleware (in Microsoft.AspNetCore.SpaServices.Webpack) that watches your client files and automatically rebuilds the bundle when a change happens.
Change the Configure method in Startup.cs to:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions { ProjectPath = Path.Combine(Directory.GetCurrentDirectory(), "ClientApp"), HotModuleReplacement = true }); app.UseDeveloperExceptionPage(); } else { app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseMvc(); }
As you can see, even hot module replacement is supported so the browser automatically reloads changed parts of your bundle without having to to a complete refresh. To make this happen we also need to add a few client libraries as well. Go to the ClientApp folder and execute:
npm install --save-dev aspnet-webpack webpack-dev-middleware webpack-hot-middleware
Now, the development environment is setup for a nice development experience. Go to the project root folder and execute:
dotnet watch run
From now on, every time a file changes, either the server executable or the client bundle is rebuilt automatically. To see the effect, change the background color in ClientApp/styles/style.css, hit save and watch the browser.
Going live
One of the goals of this exercise is to create an optimized production-ready version of our application with a single command: ‘dotnet publish’.
First however, we need to add a little bit extra webpack configuration to extract the css from the main.js bundle to its own file. With the css is in the main bundle, you might have noticed some flickering because the css is loaded after the html and JavaScript (also because the script tag is at the bottom of the _Layout.cshtml page).
We use the webpack MiniCssExtractPlugin to create the separate css bundle (from the ClientApp folder):
npm install --save-dev mini-css-extract-plugin
Change webpack.config.js to:
const path = require('path'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = (env = {}, argv = {}) => { const isProd = argv.mode === 'production'; const config = { mode: argv.mode || 'development', // we default to development when no 'mode' arg is passed entry: { main: './js/main.js' }, output: { filename: '[name].js', path: path.resolve(__dirname, '../wwwroot/dist'), publicPath: "/dist/" }, plugins: [ new MiniCssExtractPlugin({ filename: 'styles.css' }) ], module: { rules: [ { test: /\.css$/, use: [ isProd ? MiniCssExtractPlugin.loader : 'style-loader', 'css-loader' ] } ] } } return config; };
When the mode is ‘production’ (isProd), the MiniCssExtractPlugin loader is used instead of the style-loader that injects the css dynamically in the page. To load the extracted styles, a style tag is added to _Layout.cshtml:
<environment names="Staging,Production"> <link rel="stylesheet" href="~/dist/styles.css" asp-append-version="true" /> </environment>
Next, we need to add a ‘prod’ command to the scripts section in package.json to create a production bundle:
"scripts": { "build": "webpack --mode development --progress", "prod": "rimraf ../wwwroot/dist && webpack --mode production --progress" }
The ‘rimraf’ (rm –rf) command is just to clean any build artifacts from earlier builds. We don’t want those in our production bundles. Install it with npm (ClientApp folder):
npm install --save-dev rimraf
Test if the production bundles are created by running:
npm run prod
You should now see a separate styles.css file in the wwwroot/dist folder and the total file size is much smaller than with a development build (npm run build).
How do we ensure that the production bundles are created when publishing the application? How can we hook into the publishing process? Edit the .csproj file in the project root folder:
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp2.1</TargetFramework> <ClientAppFolder>ClientApp</ClientAppFolder> </PropertyGroup> <ItemGroup> <Folder Include="wwwroot\dist\" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.App" /> <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.1.0" /> </ItemGroup> <Target Name="EnsureNodeModules" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(ClientAppFolder)\node_modules') "> <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." /> <Exec WorkingDirectory="$(ClientAppFolder)" Command="npm install" /> </Target> <Target Name="BuildClientAssets" AfterTargets="ComputeFilesToPublish"> <Exec WorkingDirectory="$(ClientAppFolder)" Command="npm install" /> <Exec WorkingDirectory="$(ClientAppFolder)" Command="npm run prod" /> <!-- Include the newly-built files in the publish output --> <ItemGroup> <DistFiles Include="$(ClientAppFolder)\wwwroot\dist\**" /> <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)"> <RelativePath>%(DistFiles.Identity)</RelativePath> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> </ResolvedFileToPublish> </ItemGroup> </Target> </Project>
A new target ‘BuildClientAssets’ is executed after ComputeFilesToPublish during the publishing process. Here we generate the client bundles by executing ‘npm run prod’. Also ‘npm install’ is called to ensure the libraries are installed. After generating the bundles, these are added to the files to publish. This step is copied directly from the ASP.NET SPA templates. Big thanks to the ASP.NET developers for figuring this out :-). Now publish:
dotnet publish
Navigate to the publish output folder (/bin/Debug/netcoreapp2.1/publish), run the published application with ‘dotnet LeanAspNetCore.dll’ (the application .dll file) and navigate to https://localhost:5001. You should see the same application as before, but now as an optimized version.
Have a look at the page source in the browser and if things went right, you’ll see that the optimized .css and .js files are loaded separately and that a version querystring is added for cache busting.
Where to go from here?
The project we just created can serve as a foundation for ‘classic’ server-side ASP.NET /jQuery-style projects, but we also have everything in place to create a Single Page App or a hybrid variant.
An example of the first option can be found in the GitHub repository that contains the code for this post where an extra Form page is added with jQuery unobtrusive validation.
The next post will be an example of the second option as we add a mini React application to this project.
Pingback:Building the minimal ASP.NET Core MVC app with NPM and webpack – the 2.0 edition – Martijn Boland
Pingback:Lean ASP.NET Core 2.1 – add a React application to an existing Razor Pages application – Martijn Boland
Thanks for the post. I’ve been suffering from js/frontend tooling fatigue lately and this was a great primer getting back in the fray.
One issue though: When I try to use the WebPackDevMiddleware I get the following error after calling “dotnet run watch”:
[1] Ensure that Node.js is installed and can be found in one of the PATH directories.
Current PATH enviroment variable is:
;C:Program Filesnodejs;C:UsersAppDataRoamingnpm;C:Users.dotnettools
Make sure the Node executable is in one of those directories, or update your PATH.
[2] See the InnerException for further details of the cause.) —> System.InvalidOperationException: Failed to start Node process. To resolve this:
If I outcomment the setup code for WebPackDevMiddleware everything runs fine.
I can also verify that node is in my path – I can access both npm and node CLI in the directory.
Glad you liked the post, thanks. The issue with not finding node.js is usually something environmental. What OS are you using? Which editor? This GitHub issue (https://github.com/aspnet/JavaScriptServices/issues/707) has some potential solutions.
I’m on Windows 10, using Visual Studio 2017. But the above error occurs in my basic command prompt, not through the VS console. But I guess the call to Directory.GetCurrentDirectory() can be the culprit. I will take a look on the github issue.
Ah, the call to:
ConfigurationPath.Combine(Directory.GetCurrentDirectory(), “ClientApp”)
returns
\source\Repos\MythicArena\MythicArena:ClientApp. That’s obviously not good 😉
Solved. VS intellisense had auto-completed to ConfigurationPath.Combe() instead of Path.Combine(). Silly me.
Glad it’s solved 🙂
Oh yeah, if you’re looking for inspiration for a follow-up you could show how to get typescript working with the above setup (more precisely, with webpack). I’m going through the steps now, but I think a lot of people who are coming here will also be wanting to get typescript into the mix.
Thanks again!
Actually, the next post already involves TypeScript (and React) 🙂 https://blogs.taiga.nl/martijn/2018/06/22/lean-asp-net-core-2-1-add-a-react-application-to-an-existing-razor-pages-application/
Cool 🙂 I skipped the next one since my frontend-fatigue haven’t subsided enough for me to give React a go yet 😉 I was just searching for typescript.
But I got it up and working myself with the ts-loader. Thanks for the nice blog!
launchsettings.json file isn’t created by default when you do `dotnet new webapi`. You need to use `dotnet new webapi –use-launch-settings`
Which version of the dotnet sdk are you using (dotnet –version)? Both 2.1.300 and 2.1.302 do generate the launchSettings.json file and have a switch to exclude the launchSettings instead of including.
Ah, my mistake I was using version 2.1.202 which confusingly means 2.0 not 2.1. Installing 2.1.302 makes it behave as you describe. Sorry for the distraction.
There is one minor typo in this code here:
npm install -save-dev aspnet-webpack webpack-dev-middleware webpack-hot-middleware
It should be:
npm install –save-dev aspnet-webpack webpack-dev-middleware webpack-hot-middleware
Thanks for this, it was very informative!
Thanks for mentioning. Fixed!
No problem. Your article got me motivated to converting a project of mine to core 2.1 from mvc 5 and to show my team and to move forward with the new iteration of .net. 🙂
Good write-up, thanks. It’s probably worth mentioning that you need to include the styles.css file for the production and staging environments in _Layout.cshtml. It’s in your screen cap of the file but it think it could be worth clarifying.
Added to the post. Thanks for mentioning! I missed that one.
Great, I was struggling for several hours not knowing what was the best solution. Super.
truly invaluable post, just the right stuff I need for the hybrid approach. I love the Razor stuff and utilize it to the max, but when a complex UI pops out I shift to TS/React/Redux and develop a router of its own for the section of the app, thanks for the excellent overview and finally a walkthrough without using the default template which hides away too much with CRA