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.

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

Blogs in this Series

History

2020-06-27 Updated to ASP.NET Core 3.1, IdentityServer4 4.0.0 Angular 10.0.0
2019-08-27 Updated to ASP.NET Core 3.0, Angular 8.2.3
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": "7.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": "npm run webpack-dev",
        "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": "eslint . --ext .ts"
    },
    "dependencies": {
        "@angular/animations": "~10.0.0",
        "@angular/common": "~10.0.0",
        "@angular/compiler": "~10.0.0",
        "@angular/core": "~10.0.0",
        "@angular/forms": "~10.0.0",
        "@angular/platform-browser": "~10.0.0",
        "@angular/platform-browser-dynamic": "~10.0.0",
        "@angular/router": "~10.0.0",
        "@popperjs/core": "^2.4.2",
        "bootstrap": "4.5.0",
        "core-js": "^2.6.5",
        "ie-shim": "0.1.0",
        "rxjs": "~6.5.5",
        "rxjs-compat": "^6.5.5",
        "zone.js": "~0.10.3"
    },
    "devDependencies": {
        "@angular-devkit/build-angular": "^0.1000.0",
        "@angular/cli": "~10.0.0",
        "@angular/compiler-cli": "~10.0.0",
        "@angular/language-service": "~10.0.0",
        "@ngtools/webpack": "^10.0.0",
        "@types/jasmine": "~3.5.11",
        "@types/jasminewd2": "~2.0.8",
        "@types/node": "~14.0.14",
        "angular-router-loader": "0.8.5",
        "angular2-template-loader": "^0.6.2",
        "awesome-typescript-loader": "^5.2.1",
        "clean-webpack-plugin": "2.0.2",
        "codelyzer": "~5.2.2",
        "concurrently": "^5.2.0",
        "copy-webpack-plugin": "^5.1.1",
        "css-loader": "^3.4.2",
        "file-loader": "^3.0.1",
        "html-webpack-plugin": "^3.2.0",
        "eslint": "^7.3.1",
        "jasmine-core": "~3.5.0",
        "jasmine-spec-reporter": "~5.0.2",
        "jquery": "^3.5.1",
        "json-loader": "^0.5.7",
        "karma": "~5.1.0",
        "karma-chrome-launcher": "~3.1.0",
        "karma-coverage-istanbul-reporter": "~3.0.3",
        "karma-jasmine": "~3.3.1",
        "karma-jasmine-html-reporter": "^1.5.4",
        "karma-sourcemap-loader": "^0.3.7",
        "karma-spec-reporter": "^0.0.32",
        "karma-webpack": "4.0.2",
        "protractor": "~6.0.0",
        "raw-loader": "^1.0.0",
        "node-sass": "^4.14.1",
        "sass-loader": "^8.0.2",
        "rimraf": "^3.0.2",
        "source-map-loader": "^0.2.4",
        "style-loader": "^1.1.3",
        "toposort": "2.0.2",
        "url-loader": "^1.1.2",
        "ts-node": "~8.10.2",
        "@typescript-eslint/eslint-plugin-tslint": "^3.4.0",
        "@typescript-eslint/eslint-plugin": "^3.4.0",
        "@typescript-eslint/parser": "^3.4.0",
        "uglifyjs-webpack-plugin": "^2.2.0",
        "typescript": "~3.9.5",
        "webpack": "^4.43.0",
        "webpack-bundle-analyzer": "^3.8.0",
        "webpack-cli": "3.3.12",
        "webpack-dev-server": "^3.11.0",
        "webpack-filter-warnings-plugin": "^1.2.1"
    },
    "-vs-binding": {
        "ProjectOpened": [
            "watch-webpack-dev"
        ]
    }
}

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 webpack.prod.js 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')
    .BundleAnalyzerPlugin;
const helpers = require('./webpack.helpers');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const FilterWarningsPlugin = require('webpack-filter-warnings-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',
		  sourceMap: 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({
            cleanOnceBeforeBuildPatterns: ['./wwwroot/dist', './wwwroot/assets'],
            root: ROOT
        }),
        new webpack.NoEmitOnErrorsPlugin(),

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

	new HtmlWebpackPlugin({
		filename: '../Views/Shared/_Layout.cshtml',
		inject: 'body',
		template: 'angularApp/_Layout.cshtml'
	}),

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

        new FilterWarningsPlugin({
            exclude: /System.import/,
        }),
    ],
};

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

using System;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using AspNetCoreMvcAngular.Repositories.Things;
using Microsoft.AspNetCore.Http;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;

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

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(options => {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
            .AddCookie()
            .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.Scope.Add("thingsscope");
                options.Scope.Add("profile");
                options.Prompt = "login"; // select_account login consent
                options.SaveTokens = true;
            });

            // TODO add policies 
            services.AddAuthorization();

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

            services.AddControllersWithViews()
                .AddNewtonsoftJson()
                .SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IAntiforgery antiforgery)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            //Registered before static files to always set header
            app.UseXContentTypeOptions();
            app.UseReferrerPolicy(opts => opts.NoReferrer());

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

            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

         
            var angularRoutes = new[] {
                 "/default",
                 "/about"
             };

            app.UseDefaultFiles();
            app.UseStaticFiles();

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

            app.UseRedirectValidation(t => t.AllowSameHostRedirectsToHttps(44348)); 
            app.UseXXssProtection(options => options.EnabledWithBlockMode());

            app.UseRouting();
            app.UseAuthentication();
            app.UseAuthorization();

            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 });
                }

                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.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{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.

Note:
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.

Links:

Angular Templates, Seeds, Starter Kits

https://github.com/damienbod/AngularWebpackVisualStudio

ASP.NET Core, Angular with Webpack and Visual Studio

https://github.com/aspnet/JavaScriptServices

7 comments

  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? https://stackoverflow.com/questions/44878351/angular-2-and-asp-net-core-server-rendering. Thank you

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

Leave a comment

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