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

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": "2.0.3",
  "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 --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-watch": "karma start --no-single-run",
    "lint": "tslint ./angularApp"
  },
  "dependencies": {
    "@angular/animations": "4.4.3",
    "@angular/common": "4.4.3",
    "@angular/compiler": "4.4.3",
    "@angular/compiler-cli": "4.4.3",
    "@angular/core": "4.4.3",
    "@angular/forms": "4.4.3",
    "@angular/http": "4.4.3",
    "@angular/platform-browser": "4.4.3",
    "@angular/platform-browser-dynamic": "4.4.3",
    "@angular/platform-server": "4.4.3",
    "@angular/router": "4.4.3",
    "@angular/upgrade": "4.4.3",
    "angular-in-memory-web-api": "0.3.2",
    "bootstrap": "3.3.7",
    "core-js": "2.5.1",
    "ie-shim": "0.1.0",
    "rxjs": "5.4.3",
    "zone.js": "0.8.16"
  },
  "devDependencies": {
    "@ngtools/webpack": "^1.7.1",
    "@types/node": "^8.0.28",
    "@types/jasmine": "^2.6.0",
    "angular-router-loader": "^0.6.0",
    "angular2-template-loader": "^0.6.2",
    "awesome-typescript-loader": "^3.2.2",
    "clean-webpack-plugin": "^0.1.16",
    "codelyzer": "^3.1.2",
    "concurrently": "^3.5.0",
    "copy-webpack-plugin": "^4.0.1",
    "css-loader": "^0.28.4",
    "file-loader": "^0.11.2",
    "html-webpack-plugin": "^2.30.1",
    "jasmine-core": "^2.7.0",
    "jquery": "^3.2.1",
    "json-loader": "^0.5.7",
    "karma": "^1.7.0",
    "karma-chrome-launcher": "^2.2.0",
    "karma-jasmine": "^1.1.0",
    "karma-sourcemap-loader": "^0.3.7",
    "karma-spec-reporter": "^0.0.31",
    "karma-webpack": "^2.0.4",
    "node-sass": "^4.5.3",
    "raw-loader": "^0.5.1",
    "rimraf": "^2.6.1",
    "sass-loader": "^6.0.6",
    "source-map-loader": "^0.2.1",
    "style-loader": "^0.18.2",
    "tslint": "^5.6.0",
    "tslint-loader": "^3.5.3",
    "typescript": "^2.5.2",
    "url-loader": "^0.5.9",
    "webpack": "^3.6.0",
    "webpack-dev-server": "^2.7.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.

var path = require('path');

var webpack = require('webpack');

var HtmlWebpackPlugin = require('html-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');
var CleanWebpackPlugin = require('clean-webpack-plugin');
var helpers = require('./webpack.helpers');

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

module.exports = {

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

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

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

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

    module: {
        rules: [
            {
                test: /\.ts$/,
                loaders: [
                    'awesome-typescript-loader',
                    'angular-router-loader?aot=true&genDir=aot/'
                ]
            },
            {
                test: /\.(png|jpg|gif|woff|woff2|ttf|svg|eot)$/,
                loader: 'file-loader?name=assets/[name]-[hash:6].[ext]'
            },
            {
                test: /favicon.ico$/,
                loader: 'file-loader?name=/[name].[ext]'
            },
            {
                test: /\.css$/,
                loader: 'style-loader!css-loader'
            },
            {
                test: /\.scss$/,
                exclude: /node_modules/,
                loaders: ['style-loader', 'css-loader', 'sass-loader']
            },
            {
                test: /\.html$/,
                loader: 'raw-loader'
            }
        ],
        exprContextCritical: false
    },

    plugins: [
        new CleanWebpackPlugin(
            [
                './wwwroot/dist',
                './wwwroot/assets'
            ]
        ),
        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: '../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 System;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using AspNetCoreMvcAngular.Repositories.Things;
using Microsoft.AspNetCore.Http;
using System.IdentityModel.Tokens.Jwt;
using Serilog.Core;
using Serilog.Events;
using Serilog;
using Microsoft.AspNetCore.Antiforgery;
using System.Collections.Generic;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

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()
                .MinimumLevel.ControlledBy(MyLoggingLevelSwitch)
                .Enrich.WithProperty("App", "AspNetCoreMvcAngular")
                .Enrich.FromLogContext()
                .WriteTo.Seq("http://localhost:5341")
                .WriteTo.RollingFile("../Logs/AspNetCoreMvcAngular")
                .CreateLogger();

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

        public IConfigurationRoot 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.SaveTokens = true;
            });

            // TODO add policies 
            services.AddAuthorization();

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

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IAntiforgery antiforgery)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();
            loggerFactory.AddSerilog();

            //Registered before static files to always set header
            app.UseHsts(hsts => hsts.MaxAge(365).IncludeSubdomains());
            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();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

          

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

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

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();

            //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.UseAuthentication();

            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 =>
            {
                routes.MapRoute(
                    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.

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

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

https://github.com/aspnet/JavaScriptServices

Advertisements

6 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

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s

%d bloggers like this: