Using Angular in an ASP.NET Core View with Webpack

This article shows how Angular can be run inside an ASP.NET Core MVC view using Webpack to build the Angular application. By using Webpack, the Angular application can be built using the AOT and Angular lazy loading features and also profit from the advantages of using a server side rendered view. If you prefer to separate the SPA and the server into 2 applications, use Angular CLI or a similiar template.


Blogs in this Series


2018-06-16 Updated to ASP.NET Core 2.1, Angular 6.0.5
2017-11-05 Updated to Angular 5 and Typescript 2.6.1
2017-09-22 Updated to ASP.NET Core 2.0, Angular 4.4.3

The application was created using the .NET Core ASP.NET Core application template in Visual Studio 2017. A packages.json npm file was added to the project. The file contains the frontend build scripts as well as the npm packages required to build the application using Webpack and also the Angular packages.

  "name": "angular-webpack-visualstudio",
  "version": "5.0.0",
  "description": "An Angular VS template",
  "main": "wwwroot/index.html",
  "author": "",
  "license": "ISC",
  "repository": {
    "type": "git",
    "url": ""
  "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": "6.0.5",
    "@angular/common": "6.0.5",
    "@angular/compiler": "6.0.5",
    "@angular/compiler-cli": "6.0.5",
    "@angular/core": "6.0.5",
    "@angular/forms": "6.0.5",
    "@angular/http": "6.0.5",
    "@angular/platform-browser": "6.0.5",
    "@angular/platform-browser-dynamic": "6.0.5",
    "@angular/platform-server": "6.0.5",
    "@angular/router": "6.0.5",
    "@angular/upgrade": "6.0.5",
    "@angular-devkit/core": "0.6.8",
    "bootstrap": "3.3.7",
    "core-js": "2.5.7",
    "ie-shim": "0.1.0",
    "rxjs": "6.2.1",
    "rxjs-compat": "6.2.1",
    "zone.js": "0.8.26"
  "devDependencies": {
    "@ngtools/webpack": "^6.0.8",
    "@types/jasmine": "^2.8.8",
    "@types/node": "^10.3.3",
    "angular-router-loader": "0.8.5",
    "angular2-template-loader": "^0.6.2",
    "awesome-typescript-loader": "^5.0.0",
    "clean-webpack-plugin": "^0.1.19",
    "codelyzer": "^4.3.0",
    "concurrently": "^3.5.1",
    "copy-webpack-plugin": "^4.5.1",
    "css-loader": "^0.28.11",
    "file-loader": "^1.1.11",
    "html-webpack-plugin": "^3.2.0",
    "jasmine-core": "3.1.0",
    "jquery": "^3.3.1",
    "json-loader": "^0.5.7",
    "karma": "^2.0.3",
    "karma-chrome-launcher": "^2.2.0",
    "karma-jasmine": "^1.1.2",
    "karma-jasmine-html-reporter": "^1.1.0",
    "karma-sourcemap-loader": "^0.3.7",
    "karma-spec-reporter": "^0.0.32",
    "karma-webpack": "^3.0.0",
    "node-sass": "^4.9.0",
    "raw-loader": "^0.5.1",
    "rimraf": "^2.6.2",
    "sass-loader": "^7.0.3",
    "source-map-loader": "^0.2.3",
    "style-loader": "^0.21.0",
    "tslint": "^5.10.0",
    "tslint-loader": "^3.6.0",
    "typescript": "2.7.2",
    "uglifyjs-webpack-plugin": "^1.2.5",
    "url-loader": "^1.0.1",
    "webpack": "^4.12.0",
    "webpack-bundle-analyzer": "^2.13.1",
    "webpack-cli": "3.0.7",
    "webpack-dev-server": "^3.1.4"
  "-vs-binding": {
    "ProjectOpened": [

The angular application is added to the angularApp folder. This frontend app implements a default module and also a second about module which is lazy loaded when required (About button clicked). See Angular Lazy Loading with Webpack 2 for further details.

The _Layout.cshtml MVC View is also added here as a template. This will be used to build into the MVC application in the Views folder.

The uses all the Angular project files and builds them into pre-compiled AOT bundles, and also a separate bundle for the about module which is lazy loaded. Webpack adds the built bundles to the _Layout.cshtml template and copies this to the Views/Shared/_Layout.cshtml file.

const path = require('path');
const rxPaths = require('rxjs/_esm5/path-mapping');
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')
const helpers = require('./webpack.helpers');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');

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

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

module.exports = {
  mode: 'production',
  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'],
    alias: rxPaths()

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

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

    // new webpack.optimize.ModuleConcatenationPlugin(),

    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
      'window.jQuery': 'jquery'

    new CleanWebpackPlugin(['./wwwroot/dist', './wwwroot/assets'], {
      root: ROOT
    new webpack.NoEmitOnErrorsPlugin(),

    // new UglifyJSPlugin({
    //   parallel: 2
    // }),

    // new webpack.optimize.CommonsChunkPlugin({
    //   name: ['vendor', 'polyfills']
    // }),
	new HtmlWebpackPlugin({
		filename: '../Views/Shared/_Layout.cshtml',
		inject: 'body',
		template: 'angularApp/_Layout.cshtml'

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

The Startup.cs is configured to load the configuration and middlerware for the application using client or server routing as required.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using IdentityServerWithAspNetIdentity.Data;
using IdentityServerWithAspNetIdentity.Models;
using IdentityServerWithAspNetIdentity.Services;
using QuickstartIdentityServer;
using IdentityServer4.Services;
using System.Security.Cryptography.X509Certificates;
using System.IO;
using Microsoft.AspNetCore.Identity;
using Serilog.Core;
using Serilog.Events;
using Serilog;
using Microsoft.AspNetCore.Mvc;

namespace AspNetCoreMvcAngular
    public class Startup
        public static LoggingLevelSwitch MyLoggingLevelSwitch { get; set; }

        public Startup(IHostingEnvironment env)
            MyLoggingLevelSwitch = new LoggingLevelSwitch();
            MyLoggingLevelSwitch.MinimumLevel = LogEventLevel.Verbose;

            Log.Logger = new LoggerConfiguration()
                .Enrich.WithProperty("App", "AspNetCoreMvcAngular")

            var builder = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
            Configuration = builder.Build();

        public IConfigurationRoot Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
            services.AddAuthentication(options => {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            .AddOpenIdConnect(options =>
                options.SignInScheme = "Cookies";
                options.Authority = "https://localhost:44348";
                options.RequireHttpsMetadata = true;
                options.ClientId = "angularmvcmixedclient";
                options.ClientSecret = "thingsscopeSecret";
                options.ResponseType = "code id_token";
                options.SaveTokens = true;

            // TODO add policies 

            services.AddSingleton<IThingsRepository, ThingsRepository>();
            services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IAntiforgery antiforgery)

            //Registered before static files to always set header
            app.UseHsts(hsts => hsts.MaxAge(365).IncludeSubdomains());
            app.UseReferrerPolicy(opts => opts.NoReferrer());

            app.UseCsp(opts => opts
                .ScriptSources(s => s.Self()).ScriptSources(s => s.UnsafeEval())
                .StyleSources(s => s.UnsafeInline())


            if (env.IsDevelopment())


            var angularRoutes = new[] {


            if (env.IsDevelopment())


            //Registered after static files, to set headers for dynamic content.
            app.UseXfo(xfo => xfo.Deny());

            // Register this earlier if there's middleware that might redirect.
            // The IdentityServer4 port needs to be added here. 
            // If the IdentityServer4 runs on a different server, this configuration needs to be changed.
            app.UseRedirectValidation(t => t.AllowSameHostRedirectsToHttps(44348)); 

            app.UseXXssProtection(options => options.EnabledWithBlockMode());


            app.Use(async (context, next) =>
                string path = context.Request.Path.Value;
                if (path != null && !path.ToLower().Contains("/api"))
                    // XSRF-TOKEN used by angular in the $http if provided
                    var tokens = antiforgery.GetAndStoreTokens(context);
                    context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions { HttpOnly = false, Secure = true });

                if (context.Request.Path.HasValue && null != angularRoutes.FirstOrDefault(
                    (ar) => context.Request.Path.Value.StartsWith(ar, StringComparison.OrdinalIgnoreCase)))
                    context.Request.Path = new PathString("/");

                await next();
            app.UseMvc(routes =>
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");

The application can be built and run using the command line. The client application needs to be built before you can deploy or run!

> npm install
> npm run build-production
> dotnet restore
> dotnet run

You can also build inside Visual Studio 2017 using the Task Runner Explorer. If building inside Visual Studio 2017, you need to configure the NodeJS path correctly to use the right version.

Now you have to best of both worlds in the UI.

You could also use Microsoft ASP.NET Core JavaScript Services which supports server side pre rendering but not client side lazy loading. If your using Microsoft ASP.NET Core JavaScript Services, configure the application to use AOT builds for the Angular template.


Angular Templates, Seeds, Starter Kits


  1. […] Using Angular in an ASP.NET Core View with Webpack (Damien Bowden) […]

  2. […] Using Angular in an ASP.NET Core View with Webpack (Damien Bowden) […]

  3. Thanks for this!

  4. What does the first two comments do they just say the blog title…? Im new to wordpress so maby a silly question lol

  5. […] Utiliser Angular dans une vue ASP.NET Core avec Webpack. […]

  6. Exec21 · · Reply

    Hi Damian, thank you for your excellent post. I am wondering if there is any easy way to integrate this approach with angular universal. I post a question regarding this matter in stackoverflow. Would you please give some feedback? Thank you

  7. […] Using Angular in an ASP.NET Core View with Webpack by damienbod. […]

Leave a Reply

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

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

Google photo

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

Twitter picture

You are commenting using your Twitter 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: