Building production ready Angular apps with Visual Studio and ASP.NET Core

This article shows how Angular SPA apps can be built using Visual Studio and ASP.NET Core which can be used in production. Lots of articles, blogs templates exist for ASP.NET Core and Angular but very few support Angular production builds.

Although Angular is not so old, many different seeds and build templates already exist, so care should be taken when choosing the infrastructure for the Angular application. Any Angular template, seed which does not support AoT or treeshaking should NOT be used, and also any third party Angular component which does not support AoT should not be used.

This example uses webpack to build and bundle the Angular application. In the package.json, npm scripts are used to configure the different builds and can be used inside Visual Studio using the npm task runner.

Code: https://github.com/damienbod/AngularWebpackVisualStudio

Blogs in this series:

2017-11-05 Updated to Angular 5 and Typescript 2.6.1
2017.07.15: Updated to angular 4.3.0 and Webpack 3

See here for full history:
https://github.com/damienbod/AngularWebpackVisualStudio/blob/master/CHANGELOG.md

Short introduction to AoT and treeshaking

AoT

AoT stands for Ahead of Time compilation. As per definition from the Angular docs:

“With AOT, the browser downloads a pre-compiled version of the application. The browser loads executable code so it can render the application immediately, without waiting to compile the app first.”

With AoT, you have smaller packages sizes, fewer asynchronous requests and better security. All is explained very well in the Angular Docs:

https://angular.io/docs/ts/latest/cookbook/aot-compiler.html

The AoT uses the platformBrowser to bootstrap and not platformBrowserDynamic which is used for JIT, Just in Time.

// Entry point for AoT compilation.
export * from './polyfills';

import { platformBrowser } from '@angular/platform-browser';
import { enableProdMode } from '@angular/core';
import { AppModuleNgFactory } from '../aot/angular2App/app/app.module.ngfactory';

enableProdMode();

platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);

treeshaking

Treeshaking removes the unused portions of the libraries from the application, reducing the size of the application.

https://angular.io/docs/ts/latest/cookbook/aot-compiler.html

npm task runner

npm scripts can be used easily inside Visual Studio by using the npm task runner. Once installed, this needs to be configured correctly.

VS2015: Go to Tools –> Options –> Projects and Solutions –> External Web Tools and select all the checkboxes. More infomation can be found here.

In VS2017, this is slightly different:

Go to Tools –> Options –> Projects and Solutions –> Web Package Management –> External Web Tools and select all checkboxes:

vs_angular_build_01

npm scripts

The tsconfig-aot.json file builds to the aot folder.

{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": true,
    "skipLibCheck": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "lib": [
      "es2015",
      "dom"
    ]
  },
  "angularCompilerOptions": {
    "entryModule": "./angularApp/app/app.module#AppModule",
    "skipMetadataEmit": true
  }
}

build-production

The build-production npm script is used for the production build and can be used for the publish or the CI as required. The npm script used the ngc script and the webpack-production build.

"build-production": "npm run webpack-production",

webpack-production npm script:

"webpack-production": "set NODE_ENV=production&& webpack",

watch-webpack-dev

The watch build monitors the source files and builds if any file changes.

"watch-webpack-dev": "set NODE_ENV=development&& webpack --watch --color",

start (webpack-dev-server)

The start script runs the webpack-dev-server client application and also the ASPNET Core server application.

"start": "concurrently \"webpack-dev-server --hot --inline --progress --port 8080\" \"dotnet run\" ",

Any of these npm scripts can be run from the npm task runner.

vs_angular_build_02

Deployment

When deploying the application for an IIS, the build-production needs to be run, then the dotnet publish command, and then the contents can be copied to the IIS server. The publish-for-iis npm script can be used to publish. The command can be started from a build server without problem.

"publish-for-iis": "npm run build-production && dotnet publish -c Release" 

vs_angular_build_02

https://docs.microsoft.com/en-us/dotnet/articles/core/tools/dotnet-publish

When deploying to an IIS, you need to install the DotNetCore.1.1.0-WindowsHosting.exe for the IIS. Setting up the server IIS docs:

https://docs.microsoft.com/en-us/aspnet/core/publishing/iis

Why not webpack task runner?

The Webpack task runner cannot be used for Webpack Angular applications because it does not support the required commands for Angular Webpack builds, either dev or production. The webpack -d build causes map errors in IE and the ngc compiler cannot be used, hence no production builds can be started from the Webpack Task Runner. For Angular Webpack projects, do not use the Webpack Task Runner, use the npm task runner.

Full package.json

{
  "name": "angular-webpack-visualstudio",
  "version": "3.0.0",
  "description": "An Angular VS template",
  "main": "wwwroot/index.html",
  "author": "",
  "license": "ISC",
  "repository": {
    "type": "git",
    "url": "https://github.com/damienbod/Angular2WebpackVisualStudio.git"
  },
  "scripts": {
    "start": "concurrently \"webpack-dev-server --env=dev --open --hot --inline --port 8080\" \"dotnet run\" ",
    "webpack-dev": "webpack --env=dev",
    "webpack-production": "webpack --env=prod",
    "build-dev": "npm run webpack-dev",
    "build-production": "npm run webpack-production",
    "watch-webpack-dev": "webpack --env=dev --watch --color",
    "watch-webpack-production": "npm run build-production --watch --color",
    "publish-for-iis": "npm run build-production && dotnet publish -c Release",
    "test": "karma start",
    "test-ci": "karma start --single-run --browsers ChromeHeadless",
    "lint": "tslint ./angularApp"
  },
  "dependencies": {
    "@angular/animations": "5.0.0",
    "@angular/common": "5.0.0",
    "@angular/compiler": "5.0.0",
    "@angular/compiler-cli": "5.0.0",
    "@angular/core": "5.0.0",
    "@angular/forms": "5.0.0",
    "@angular/http": "5.0.0",
    "@angular/platform-browser": "5.0.0",
    "@angular/platform-browser-dynamic": "5.0.0",
    "@angular/platform-server": "5.0.0",
    "@angular/router": "5.0.0",
    "@angular/upgrade": "5.0.0",
    "bootstrap": "3.3.7",
    "core-js": "2.5.1",
    "ie-shim": "0.1.0",
    "rxjs": "5.5.2",
    "zone.js": "0.8.18"
  },
  "devDependencies": {
    "@ngtools/webpack": "^1.8.0",
    "@types/jasmine": "^2.6.2",
    "@types/node": "^8.0.47",
    "angular-router-loader": "^0.6.0",
    "angular2-template-loader": "^0.6.2",
    "awesome-typescript-loader": "^3.3.0",
    "clean-webpack-plugin": "^0.1.17",
    "codelyzer": "^4.0.0",
    "concurrently": "^3.5.0",
    "copy-webpack-plugin": "^4.2.0",
    "css-loader": "^0.28.7",
    "file-loader": "^1.1.5",
    "html-webpack-plugin": "^2.30.1",
    "jasmine-core": "^2.8.0",
    "jquery": "^3.2.1",
    "json-loader": "^0.5.7",
    "karma": "^1.7.1",
    "karma-chrome-launcher": "^2.2.0",
    "karma-jasmine": "^1.1.0",
    "karma-jasmine-html-reporter": "^0.2.2",
    "karma-sourcemap-loader": "^0.3.7",
    "karma-spec-reporter": "^0.0.31",
    "karma-webpack": "^2.0.5",
    "node-sass": "^4.5.3",
    "raw-loader": "^0.5.1",
    "rimraf": "^2.6.2",
    "sass-loader": "^6.0.6",
    "source-map-loader": "^0.2.3",
    "style-loader": "^0.19.0",
    "tslint": "^5.8.0",
    "tslint-loader": "^3.5.3",
    "typescript": "^2.6.1",
    "url-loader": "^0.6.2",
    "webpack": "^3.8.1",
    "webpack-bundle-analyzer": "^2.9.0",
    "webpack-dev-server": "^2.9.3"
  },
  "-vs-binding": {
    "ProjectOpened": [
      "watch-webpack-dev"
    ]
  }
}

Full webpack.prod.js

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpackTools = require('@ngtools/webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const helpers = require('./webpack.helpers');

const ROOT = path.resolve(__dirname, '..');

console.log('@@@@@@@@@ USING PRODUCTION @@@@@@@@@@@@@@@');

module.exports = {

    entry: {
        'polyfills': './angularApp/polyfills.ts',
        'vendor': './angularApp/vendor.ts',
        'app': './angularApp/main-aot.ts'
    },

    output: {
        path: ROOT + '/wwwroot/',
        filename: 'dist/[name].[hash].bundle.js',
        chunkFilename: 'dist/[id].[hash].chunk.js',
        publicPath: '/'
    },

    resolve: {
        extensions: ['.ts', '.js', '.json']
    },

    devServer: {
        historyApiFallback: true,
        stats: 'minimal',
        outputPath: path.join(ROOT, 'wwwroot/')
    },

    module: {
        rules: [
            {
                test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/,
                use: '@ngtools/webpack'
            },
            {
                test: /\.(png|jpg|gif|woff|woff2|ttf|svg|eot)$/,
                use: 'file-loader?name=assets/[name]-[hash:6].[ext]'
            },
            {
                test: /favicon.ico$/,
                use: 'file-loader?name=/[name].[ext]'
            },
            {
                test: /\.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /\.scss$/,
                include: path.join(ROOT, 'angularApp/styles'),
                use: [
                    'style-loader',
                    'css-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.scss$/,
                exclude: path.join(ROOT, 'angularApp/styles'),
                use: [
                    'raw-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.html$/,
                use: 'raw-loader'
            }
        ],
        exprContextCritical: false
    },
    plugins: [
        //new BundleAnalyzerPlugin({
        //    analyzerMode: 'static'
        //}),
        new webpackTools.AngularCompilerPlugin({
            tsConfigPath: './tsconfig-aot.json'
            // entryModule: './angularApp/app/app.module#AppModule'
        }),

        new CleanWebpackPlugin(
            [
                './wwwroot/dist',
                './wwwroot/assets'
            ],
            { root: ROOT }
        ),
        new webpack.NoEmitOnErrorsPlugin(),
        new webpack.optimize.UglifyJsPlugin({
            compress: {
                warnings: false
            },
            output: {
                comments: false
            },
            sourceMap: false
        }),
        new webpack.optimize.CommonsChunkPlugin(
            {
                name: ['vendor', 'polyfills']
            }),
        new HtmlWebpackPlugin({
            filename: 'index.html',
            inject: 'body',
            template: 'angularApp/index.html'
        }),

        new CopyWebpackPlugin([
            { from: './angularApp/images/*.*', to: 'assets/', flatten: true }
        ])
    ]

};


Links:

https://damienbod.com/2016/06/12/asp-net-core-angular2-with-webpack-and-visual-studio/

https://github.com/preboot/angular2-webpack

https://webpack.github.io/docs/

https://github.com/jtangelder/sass-loader

https://github.com/petehunt/webpack-howto/blob/master/README.md

https://blogs.msdn.microsoft.com/webdev/2015/03/19/customize-external-web-tools-in-visual-studio-2015/

https://marketplace.visualstudio.com/items?itemName=MadsKristensen.NPMTaskRunner

http://sass-lang.com/

http://blog.thoughtram.io/angular/2016/06/08/component-relative-paths-in-angular-2.html

https://angular.io/docs/ts/latest/guide/webpack.html

http://blog.mgechev.com/2016/06/26/tree-shaking-angular2-production-build-rollup-javascript/

https://angular.io/docs/ts/latest/tutorial/toh-pt5.html

http://angularjs.blogspot.ch/2016/06/improvements-coming-for-routing-in.html?platform=hootsuite

https://angular.io/docs/ts/latest/cookbook/aot-compiler.html

https://docs.microsoft.com/en-us/aspnet/core/publishing/iis

https://weblog.west-wind.com/posts/2016/Jun/06/Publishing-and-Running-ASPNET-Core-Applications-with-IIS

http://blog.mgechev.com/2017/01/17/angular-in-production/

29 comments

  1. Really terrific work. Thanks!

  2. ambarki youssef · · Reply

    Very interesting example that works.
    Thank you very much!!!!
    I am a little confused because a lot of tools are used.
    I wish to customize bootstrap by modifying the variable variables variable.less.
    Can you tell me how I can do it?

    1. thanks, which variable?

      main.ts is the bootstrap for dev, main-aot.ts is the production bootstrap.

      Greetings Damien

  3. What do you think about this yeoman generator: generator-aspnetcore-spa which is developed by Steve Sanderson from the ASP.NET team: http://blog.stevensanderson.com/2016/05/02/angular2-react-knockout-apps-on-aspnet-core/

    1. Steve is doing great work here.

  4. Thanks for sharing the article. However, I noticed one thing that twitter bootstrap intellisense is not working while code for HTML file. What could be the reason?

  5. First off – Angular2WebpackVisualStudio VERY nice and what I had been looking for. But a few newbie questions.
    • In the Task Runner Explorer there are tasks defined ( like build-dev, etc) from the package.json and I see them define in the json file. But where do the tasks in the webpage.config.js come from? Like hot, cold, etc.
    • What is the difference between all these options.
    • If I run ‘hot’, it runs webpack-server-dev and love that I can make my changes in ts and see them automatically in the browser but the webapi doesn’t work. I need to run this from dotnet with dotnet run, the webservice works but now I do not get the auto loading. Is there a way to get both?

    Again, awesome work, this helped me a ton.

  6. I just figured out npm start which answers my last question.

  7. pdunlop · · Reply

    Thanks Damien, this is great!

    Have you also tried to run unit tests along side this configuration? I have it (sort of) working with karma-jasmine plug-in but I can’t seem to get test coverage reports with istanbul to work. Do you have any recommendations regarding testing?

    Thanks, Pete.

    1. Hi Pete

      No, let me know if you have any recommendations, would like to add one
      Greetings Damien

  8. […] Building production ready Angular apps with Visual Studio and ASP.NET Core (Damien Bowden) […]

  9. […] Building production ready Angular apps with Visual Studio and ASP.NET Core […]

  10. Ralph Krausse · · Reply

    Hello Damien,

    I do have another question. If webpack bundles up all your files, how do you go about debuging them? For examle, I am calling a webservice and I need to see the return in the debugger in chrome ( I know I can’t debug this in VS ). Do I just search in the bundled file or is there a way to not bundle for debugging reasons.

    Thanks
    Ralph

    1. The dev build bundles the map files so that you can debug. I’ll do a blog about this soon

      Greetings Damien

  11. Sharif · · Reply

    Hi,
    I am trying to build your project in production mode. But it is showing the following compilation error 👍

    ERROR in ./angularApp/app/app.routes.ts
    Module not found: Error: Can’t resolve ‘…..\aot\angularApp\app\about\about.module.ngfactory’ in ‘C:\Users\Dell\documents\visual studio 2015\Projects\Anguar2WebPack\src\Anguar2WebPack\angularApp\app’
    @ ./angularApp/app/app.routes.ts 5:139-212
    @ ./angularApp/app/app.module.ts
    @ ./angularApp/app/app.module.ngfactory.ts
    @ ./angularApp/main-aot.ts

    I don’t have any clue why is this happening. can any one help me about this.
    Here is my main.aot.ts file:

    import { platformBrowser } from ‘@angular/platform-browser’;
    import {AppModuleNgFactory } from ‘./app/app.module.ngfactory’;
    import {AboutModuleNgFactory } from “./app/about/about.module.ngfactory”;

    platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
    Up to this everything is ok.
    Here is my app.route.ts:

    import { Routes, RouterModule } from ‘@angular/router’;

    export const routes: Routes = [
    { path: ”, redirectTo: ‘home’, pathMatch: ‘full’ },
    {
    path: ‘about’, loadChildren: ‘./about/about.module#AboutModule’,
    }
    ];

    export const AppRoutes = RouterModule.forRoot(routes);
    Here the problem causing the “./about/about.module#AboutModule”
    it cannot load the AboutModule and getting the above error.

    1. HI Sharif, try building the SPA from the console
      > npm install
      > npm run build-production
      > dotnet restore
      > dotnet run

      Greetings Damien

  12. Hi Damien

    Really great template. Was wondering if you had any pointers on deploying to Azure. We can run locally just fine but when we use a deployment build definition in Azure for continuous deployment we are getting a 502 bad gateway error. Any assistance would be much appreciated. You can contact me directly if you wish. Thank you.

  13. RenatoSc · · Reply

    Really nice work.
    It helped me a lot! Thanks!

    Is it possible to use angular-cli with this solution? What would I have to change?

  14. can this be deployed on Azure?

    1. Yes, you have different options , here’s one example,

      https://github.com/damienbod/AspNetCoreDockerAzureDemo

      The url configuration and the security are the 2 issues which you still need to solve

      Greetings Damien

  15. George · · Reply

    Hi Damian, Thanks for all the great work and help you have provided. I have a question though regarding SASS, AOT and Webpack. How did you get it to work in components. Other than importing the scss file. What must be done so that the component picks up the styles. Thanks again for you help. -George

    1. Thanks

      When using scss files, it only worked when using the import. Did not find another way to do this.

      Greetings Damien

  16. Nice work Dear. One issue after publish in iis its not loading the script mentioned in index.html.
    Virtual folder name : ajs4

    file path expected: http://localhost/ajs4/dist/polyfills.bundle.js
    searching for : http://localhost/dist/polyfills.bundle.js

    same for all scripts.

    Running correctly while debug in Visual studio.

    Please let me know where do i change to work in iis.?

    1. This path is set in the webpack.prod.js file. You need to change this. It is then built into the indel.html deployed in the wwwroot folder. Greetings Damien

  17. radeldudel · · Reply

    How do you call enableProdMode(); in production mode but not in development mode? I got no idea how to know which mode Angular is running in.

  18. […] Building production ready Angular apps with Visual Studio and ASP.NET Core by Damien Bod. […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: