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
- Using Angular in an ASP.NET Core View with Webpack
- Secure ASP.NET Core MVC with Angular using IdentityServer4 OpenID Connect Hybrid Flow
- Anti-Forgery Validation with ASP.NET Core MVC and Angular
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": "1.0.0", "description": "An Angular VS template", "author": "", "license": "ISC", "repository": { "type": "git", "url": "https://github.com/damienbod/Angular2WebpackVisualStudio.git" }, "scripts": { "ngc": "ngc -p ./tsconfig-aot.json", "webpack-dev": "set NODE_ENV=development && webpack", "webpack-production": "set NODE_ENV=production && webpack", "build-dev": "npm run webpack-dev", "build-production": "npm run ngc && npm run webpack-production", "watch-webpack-dev": "set NODE_ENV=development && webpack --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" }, "dependencies": { "@angular/common": "4.1.0", "@angular/compiler": "4.1.0", "@angular/compiler-cli": "4.1.0", "@angular/platform-server": "4.1.0", "@angular/core": "4.1.0", "@angular/forms": "4.1.0", "@angular/http": "4.1.0", "@angular/platform-browser": "4.1.0", "@angular/platform-browser-dynamic": "4.1.0", "@angular/router": "4.1.0", "@angular/upgrade": "4.1.0", "@angular/animations": "4.1.0", "angular-in-memory-web-api": "0.3.1", "core-js": "2.4.1", "reflect-metadata": "0.1.10", "rxjs": "5.3.0", "zone.js": "0.8.8", "bootstrap": "^3.3.7", "ie-shim": "~0.1.0" }, "devDependencies": { "@types/node": "7.0.13", "@types/jasmine": "2.5.47", "angular2-template-loader": "0.6.2", "angular-router-loader": "^0.6.0", "awesome-typescript-loader": "3.1.2", "clean-webpack-plugin": "^0.1.16", "concurrently": "^3.4.0", "copy-webpack-plugin": "^4.0.1", "css-loader": "^0.28.0", "file-loader": "^0.11.1", "html-webpack-plugin": "^2.28.0", "jquery": "^3.2.1", "json-loader": "^0.5.4", "node-sass": "^4.5.2", "raw-loader": "^0.5.1", "rimraf": "^2.6.1", "sass-loader": "^6.0.3", "source-map-loader": "^0.2.1", "style-loader": "^0.16.1", "ts-helpers": "^1.1.2", "tslint": "^5.1.0", "tslint-loader": "^3.5.2", "typescript": "2.3.2", "url-loader": "^0.5.8", "webpack": "^2.4.1", "webpack-dev-server": "2.4.2", "jasmine-core": "2.5.2", "karma": "1.6.0", "karma-chrome-launcher": "2.0.0", "karma-jasmine": "1.1.0", "karma-sourcemap-loader": "0.3.7", "karma-spec-reporter": "0.0.31", "karma-webpack": "2.0.3" }, "-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.Collections.Generic; using System.Linq; using System.Threading.Tasks; 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; namespace AspNetCoreMvcAngular { public class Startup { public Startup(IHostingEnvironment env) { 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.AddCors(options => { options.AddPolicy("AllowAllOrigins", builder => { builder .AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod(); }); }); services.AddSingleton<IThingsRepository, ThingsRepository>(); services.AddMvc(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); var angularRoutes = new[] { "/default", "/about" }; app.Use(async (context, next) => { 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.UseCors("AllowAllOrigins"); app.UseDefaultFiles(); app.UseStaticFiles(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseStaticFiles(); 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 Angulat 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
