Click here to Skip to main content
15,071,729 members
Articles / Web Development / HTML
Article
Posted 22 Jan 2017

Stats

185.7K views
1.7K downloads
104 bookmarked

SPA^2 using ASP.NET Core 1.1 + Angular 2.4 - Part 1

Rate me:
Please Sign up or sign in to vote.
4.97/5 (63 votes)
9 Mar 2017CPOL15 min read
"... dogs and cats living together..." or how to build an application framework to get the best out of ASP.NET Core and Angular - at the same time. Updated source now with VS2015+VS2017

Introduction

This is the first of a series of articles to show how to create an application framework from scratch that uses both ASP.NET Core and Angular 2 together, using the best features of each. This method is relatively easy to use, scales well across small or large teams, and performs well for normal websites as well as multiple tenanted sites.

Applications using both ASP.NET Core and Angular 2 have been done before, but tend to either (a) barely use ASP.NET Core - just serving data using Web API and serving 'flat' HTML files as Angular templates, else (b) go to the other extreme of complexity, using ASP.NET Core with Webpack to pre-render Angular 2 on the server.

I've tried both, but found the simpler option (a) can tempt team members to cut and paste client side markup instead of creating common directives, as well as more fragile code with much less than ideal coupling. The more complex option (b) simply makes many developers' heads spin (junior and senior alike) and can take a lot of work to set up and debug. In both cases, many of the great features of ASP.NET, tag helpers, MVC views and Razor are left unused.

This framework uses ASP.NET MVC views, Razor, and tag helpers to generate client side HTML, CSS markup, validation, Angular code and Angular views all from the types and attributes on properties of your server side view model. Last of all, once the Web API data component are created, we'll use Swagger tools to create Angular 2 data services and data models, leaving you the relatively simple task of creating the Angular components to link these all together.

For these articles, I'll assume you know a little about ASP.NET MVC, general C#, HTML, Bootstrap and Angular, and concentrate on brief details on how to use these together. If you need in depth information, there are some very good tutorials here on Code Project as well as on training sites such as Pluralsight.

I've used the techniques here over the last two years in a number of commercial projects and in a wide variety of business domains. These articles take these concepts and move from .NET 4.5x and Angular 1.x to use .NET Core and Angular 2.

TLDR - for the final source, see Github at https://github.com/RobertDyball/a2spa.

(1) Create an ASP.NET Core App

To create our ASP.NET + Angular SPA we will start by creating a standard Visual Studio 2015 ASP.NET Core template, styled using Bootstrap 3.

In Visual Studio 2015, click File, New, Project.

VS 2015 - clickFile, New, Project

After File, New, Project, select Templates, then ASP.NET Core:

Provide the name (I used A2SPA), the path or Location (I used c:\dev\), I like to leave "Create directory for solution" checked, and also I check "Create new GIT repository".

Select Web Application, leave Authentication as "No Authentication"

Select Web Application, leave Authentication as "No Authentication", then click OK.

The solution read me file

You should now see the solution read me file, and a new solution. Click Ctrl-F5 on your keyboard, and you should build the solution, launch IIS Express and your default browser, and if all is well, see something like this, below, in your browser:

Test using Ctrl-F5

(2) Update ASP.NET Core from Version 1.0.1 to 1.1.0

Next, we'll update our base ASP.NET Core solution from version 1.0.1 to version 1.1.0, although by the time this is out there, there's likely to be another version, the general method should likely be similar. Have a look at the ASP.NET blogs here for details of version 1.0.1 to 1.1.0.

Open global.json, change from:

JavaScript
{
  "projects": [ "src", "test" ],
  "sdk": {
    "version": "1.0.0-preview2-003131"
  }
}

to the following, to use the latest / current ASP.NET Core SDK:

JavaScript
{
  "projects": [ "src", "test" ],
  "sdk": {
    "version": "1.0.0-preview2-1-003177"
  }
}

Open project.json and change the part of the file where it has this:

JavaScript
"dependencies": {
  "Microsoft.NETCore.App": {
    "version": "1.0.1",
    "type": "platform"
  },

To read the following, this version 1.1.0 entry corresponds to the SDK version "1.0.0-preview2-003177" immediately above:

JavaScript
"dependencies": {
  "Microsoft.NETCore.App": {
    "version": "1.1.0",
    "type": "platform"
  },

and lastly, same file, look for this:

JavaScript
"frameworks": {
  "netcoreapp1.0": {
    "imports": [
      "dotnet5.6",
      "portable-net45+win8"
    ]
  }
},

and change it to this, agani to target the new framework 1.1 instead of the earlier 1.0 framework:

JavaScript
"frameworks": {
  "netcoreapp1.1": {
    "imports": [
      "dotnet5.6",
      "portable-net45+win8"
    ]
  }
},

Next, we'll upgrade the packages using NuGet, right click the solution within the project, then click "Manage NuGet Packages".

Manage NuGet Packages

Select Updates

select Updates

Check "Select All Packages" then click the Update button to begin the update:

Update

Click Agree to proceed:

Agree

Wait to download the updates, then for update to finish:

wait...

Finally, rebuild, hit ctrl-F5 and you have a successful build and should see the app again, still running:

test running ctrl-F5 to verify project still builds and loads as it should

Package.json prior to the upgrade was:

JavaScript
{
  "dependencies": {
    "Microsoft.NETCore.App": {
      "version": "1.0.1",
      "type": "platform"
    },
    "Microsoft.AspNetCore.Diagnostics": "1.0.0",
    "Microsoft.AspNetCore.Mvc": "1.0.1",
    "Microsoft.AspNetCore.Razor.Tools": {
      "version": "1.0.0-preview2-final",
      "type": "build"
    },
    "Microsoft.AspNetCore.Routing": "1.0.1",
    "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.1",
    "Microsoft.AspNetCore.StaticFiles": "1.0.0",
    "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
    "Microsoft.Extensions.Configuration.Json": "1.0.0",
    "Microsoft.Extensions.Logging": "1.0.0",
    "Microsoft.Extensions.Logging.Console": "1.0.0",
    "Microsoft.Extensions.Logging.Debug": "1.0.0",
    "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0",
    "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0"
  },

  "tools": {
    "BundlerMinifier.Core": "2.0.238",
    "Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final",
    "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final"
  },

  "frameworks": {
    "netcoreapp1.0": {
      "imports": [
        "dotnet5.6",
        "portable-net45+win8"
      ]
    }
  },

  "buildOptions": {
    "emitEntryPoint": true,
    "preserveCompilationContext": true
  },

  "runtimeOptions": {
    "configProperties": {
      "System.GC.Server": true
    }
  },

  "publishOptions": {
    "include": [
      "wwwroot",
      "**/*.cshtml",
      "appsettings.json",
      "web.config"
    ]
  },

  "scripts": {
    "prepublish": [ "bower install", "dotnet bundle" ],
    "postpublish": [ "dotnet publish-iis 
     --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
  }
}

For reference, the final package.json should now (give or take) look like this:

JavaScript
{
  "dependencies": {
    "Microsoft.NETCore.App": {
      "version": "1.1.0",
      "type": "platform"
    },
    "BundlerMinifier.Core": "2.2.306",
    "Microsoft.AspNetCore.Diagnostics": "1.1.0",
    "Microsoft.AspNetCore.Mvc": "1.1.0",
    "Microsoft.AspNetCore.Razor.Tools": "1.1.0-preview4-final",
    "Microsoft.AspNetCore.Routing": "1.1.0",
    "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0",
    "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.1.0-preview4-final",
    "Microsoft.AspNetCore.Server.Kestrel": "1.1.0",
    "Microsoft.AspNetCore.StaticFiles": "1.1.0",
    "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0",
    "Microsoft.Extensions.Configuration.Json": "1.1.0",
    "Microsoft.Extensions.Logging": "1.1.0",
    "Microsoft.Extensions.Logging.Console": "1.1.0",
    "Microsoft.Extensions.Logging.Debug": "1.1.0",
    "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
    "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.1.0"
  },
 
  "tools": {
  },
 
  "frameworks": {
    "netcoreapp1.1": {
      "imports": [
        "dotnet5.6",
        "portable-net45+win8"
      ]
    }
  },
 
  "buildOptions": {
    "emitEntryPoint": true,
    "preserveCompilationContext": true
  },
 
  "runtimeOptions": {
    "configProperties": {
      "System.GC.Server": true
    }
  },
 
  "publishOptions": {
    "include": [
      "wwwroot",
      "**/*.cshtml",
      "appsettings.json",
      "web.config"
    ]
  },
 
  "scripts": {
    "prepublish": [ "bower install", "dotnet bundle" ],
    "postpublish": [ "dotnet publish-iis 
     --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
  }
}

(3) Adding Angular 2 QuickStart into our ASP.NET Core App

We'll be using the Angular 2 QuickStart app from the source on GitHub, here.

If you need some further background, work through the excellent tutorials from the Angular 2 team here.

To get the Angular 2 QuickStart code merged into our ASP.NET Core MVC app, we'll start by getting a copy of the source.

There are a few ways to do this; you could download a ZIP of the latest source from GitHub, or you could clone a complete copy of the repository using Git Extensions or your favourite Git utility.

The command is the same, irrespective of where you do it, in my case, I typed this into Powershell:

BAT
git clone https://github.com/angular/quickstart

This is the result:

Git Cloneto get Angular 2 source

Next navigate to the folder containing the files cloned above.

Copy the app folder (both folder and files in the folder) as well as these files: index.html, systemjs.config.extras.js and system.config.js to the VS2015 solution's wwwroot folder.

In addition, copy the files package.json, tsconfig.json and tslint.json to the VS2015 project's root directory.

copy angular 2 files

Prior to copying, your VS2015 project should look like this:

solution, before copying

If you copy files from QuickStart using Windows explorer, you can paste the files directly into the project using VS2015 solution explorer. You should see this when completed:

Completed quickstart mods

NOTE: The warning on missing dependencies is simply due to the presence of the new package.json file. Additionally in VS2015, the file systemjs.config.extras.js will be collapsed under the system.config.js - even if you copy them separately. It should be there in the wwwroot folder, and you can easily verify this by checking it.

Lastly, before you build, edit the package.json file to remove packages no longer needed.

Before editing, the package.json file should look like this, below, though the exact content may vary over time as Angular 2 progresses versions and other changes.

Before:

JavaScript
{
  "name": "angular-quickstart",
  "version": "1.0.0",
  "description": "QuickStart package.json from the documentation, 
                  supplemented with testing support",
  "scripts": {
    "start": "tsc && concurrently \"tsc -w\" \"lite-server\" ",
    "e2e": "tsc && concurrently \"http-server -s\" 
           \"protractor protractor.config.js\" --kill-others --success first",
    "lint": "tslint ./app/**/*.ts -t verbose",
    "lite": "lite-server",
    "pree2e": "webdriver-manager update",
    "test": "tsc && concurrently \"tsc -w\" \"karma start karma.conf.js\"",
    "test-once": "tsc && karma start karma.conf.js --single-run",
    "tsc": "tsc",
    "tsc:w": "tsc -w"
  },
  "keywords": [],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "@angular/common": "~2.4.0",
    "@angular/compiler": "~2.4.0",
    "@angular/core": "~2.4.0",
    "@angular/forms": "~2.4.0",
    "@angular/http": "~2.4.0",
    "@angular/platform-browser": "~2.4.0",
    "@angular/platform-browser-dynamic": "~2.4.0",
    "@angular/router": "~3.4.0",
    "angular-in-memory-web-api": "~0.2.4",
    "systemjs": "0.19.40",
    "core-js": "^2.4.1",
    "rxjs": "5.0.1",
    "zone.js": "^0.7.4"
  },
  "devDependencies": {
    "concurrently": "^3.1.0",
    "lite-server": "^2.2.2",
    "typescript": "~2.0.10",
    "canonical-path": "0.0.2",
    "http-server": "^0.9.0",
    "tslint": "^3.15.1",
    "lodash": "^4.16.4",
    "jasmine-core": "~2.4.1",
    "karma": "^1.3.0",
    "karma-chrome-launcher": "^2.0.0",
    "karma-cli": "^1.0.1",
    "karma-jasmine": "^1.0.2",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~4.0.14",
    "rimraf": "^2.5.4",
    "@types/node": "^6.0.46",
    "@types/jasmine": "^2.5.36"
  },
  "repository": {}
}

After:

JavaScript
{
  "dependencies": {
    "@angular/common": "~2.4.0",
    "@angular/compiler": "~2.4.0",
    "@angular/core": "~2.4.0",
    "@angular/forms": "~2.4.0",
    "@angular/http": "~2.4.0",
    "@angular/platform-browser": "~2.4.0",
    "@angular/platform-browser-dynamic": "~2.4.0",
    "@angular/router": "~3.4.0",
    "angular-in-memory-web-api": "~0.2.4",
    "systemjs": "0.19.40",
    "core-js": "^2.4.1",
    "rxjs": "5.0.1",
    "zone.js": "^0.7.4"
  },
  "devDependencies": {
    "typescript": "~2.0.10",
    "tslint": "^3.15.1"
  },
  "repository": {}
}

As you hit Save, after editing, you will likely see VS2015 restoring the packages:

Restoring packages

And then when finished, no more warnings beside dependencies:

Finished restoring packages

Next we'll edit tsconfig.json, from this:

JavaScript
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "lib": [ "es2015", "dom" ],
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true
  }
}

to this:

JavaScript
{
  "compilerOptions": {
    "diagnostics": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "lib": [ "es2015", "dom" ],
    "listFiles": true,
    "module": "commonjs",
    "moduleResolution": "node",
    "noImplicitAny": true,
    "outDir": "wwwroot",
    "removeComments": false,
    "rootDir": "wwwroot",
    "sourceMap": true,
    "suppressImplicitAnyIndexErrors": true,
    "target": "es5"
  },
  "exclude": [
    "node_modules"
  ]
}

Last of all, before we build, we have one typescript file to remove.

Go to the app folder under wwwroot and delete app.component.spec.ts:

Since we have not included the tests in this project, nor the dependencies, so it will create errors if left in place.

When the build is completed, you should find the typescript files automatically "transpiled" in-place, and when unfolded, reveal a JavaScript .js file as well as a map .js.map file for each.

Automatic Typescript transpiling

The .js JavaScript files will be executed in the browser, the .js.map files are used if and when you are debugging to link an issue in JavaScript back to the typescript source.

To test the build, hit Ctrl-F5 to compile and launch the browser, you should still see a working MVC page.
At the default URL:

Default URL

As well as at /home/index:

navigating to /home/index

To see the quickstart page, edit the URL to look at /index.html.

You should see the Angular loader:

Angular loader

But you will not see anything else, yet. Hit F12 and you should see Angular's error message:

Angular Error Message

Which when unfolded is likely not all too helpful either.

The reason is that none of the JavaScript libraries required are served from their current location. Look inside index.html and you see:

HTML
<!DOCTYPE html>
<html>
  <head>
    <title>Angular QuickStart</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="styles.css">
    <!-- Polyfill(s) for older browsers -->
    <script src="node_modules/core-js/client/shim.min.js"></script>
    <script src="node_modules/zone.js/dist/zone.js"></script>
    <script src="node_modules/systemjs/dist/system.src.js"></script>
    <script src="systemjs.config.js"></script>
    <script>
      System.import('app').catch(function(err){ console.error(err); });
    </script>
  </head>
  <body>
    <my-app>Loading AppComponent content here ...</my-app>
  </body>
</html>

The library files would be expected to be served from wwwroot/node_modules but they are actually under this, in the project root. This is not obvious unless you turn on the show all files option:

Show All Files

Then you will see the folder appear, greyed out to indicate it is not source controlled or part of the solution, but added separately as required.

greyed out folders
You can re-hide the folders, as they can get distracting, and now we'll fix this issue.
We'll add a package Microsoft.AspNetCore.SpaServices using NuGet.
Right click the project root, click Manage NuGet Packages, select Browse, ensure you have pre-release checked:

NuGet with Pre-Release checked

Click on the package:

Click On The Package

Then:

Package Selected

After install, don't be fooled by the Update button (look closely, you will see it is a downgrade).

Upgrade or downgrade

The important thing is that this aspnet core library is now available to use. Look at project.json and you will see the new package added there:

JavaScript
{
  "dependencies": {
    "Microsoft.NETCore.App": {
      "version": "1.1.0",
      "type": "platform"
    },
    "BundlerMinifier.Core": "2.2.306",
    "Microsoft.AspNetCore.Diagnostics": "1.1.0",
    "Microsoft.AspNetCore.Mvc": "1.1.0",
    "Microsoft.AspNetCore.Razor.Tools": "1.1.0-preview4-final",
    "Microsoft.AspNetCore.Routing": "1.1.0",
    "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0",
    "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.1.0-preview4-final",
    "Microsoft.AspNetCore.Server.Kestrel": "1.1.0",
    "Microsoft.AspNetCore.StaticFiles": "1.1.0",
    "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0",
    "Microsoft.Extensions.Configuration.Json": "1.1.0",
    "Microsoft.Extensions.Logging": "1.1.0",
    "Microsoft.Extensions.Logging.Console": "1.1.0",
    "Microsoft.Extensions.Logging.Debug": "1.1.0",
    "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
    "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.1.0",
    "Microsoft.AspNetCore.SpaServices": "1.1.0-beta-000002"
  },
 
  "tools": {
  },
 
  "frameworks": {
    "netcoreapp1.1": {
      "imports": [
        "dotnet5.6",
        "portable-net45+win8"
      ]
    }
  },
 
  "buildOptions": {
    "emitEntryPoint": true,
    "preserveCompilationContext": true
  },
 
  "runtimeOptions": {
    "configProperties": {
      "System.GC.Server": true
    }
  },
 
  "publishOptions": {
    "include": [
      "wwwroot",
      "**/*.cshtml",
      "appsettings.json",
      "web.config"
    ]
  },
 
  "scripts": {
    "prepublish": [ "bower install", "dotnet bundle" ],
    "postpublish": [ "dotnet publish-iis 
     --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
  }
}

Next open the file startup.cs in the editor, where we will edit the end of the file, where it has:

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

Change this to:

C#
...
            app.UseDefaultFiles();
            app.UseStaticFiles();
            app.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider
                (Path.Combine(env.ContentRootPath, "node_modules")),
                RequestPath = "/node_modules"
            });
 
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
 
                // in case multiple SPAs required.
                routes.MapSpaFallbackRoute("spa-fallback", 
                new { controller = "home", action = "index" });
            });
        }
    }
}

To satisfy dependencies for PhysicalFileProvider and Path.Combine, we need to add this to the usings at the top of startup.cs:

Before:

C#
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;
 
namespace A2SPA
...

After:

C#
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 Microsoft.Extensions.FileProviders;
using System.IO;
 
namespace A2SPA
...

Some of the default dependency entries are not required, so you can edit these to this final version:

C#
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.FileProviders;
using System.IO;
 
namespace A2SPA
...

Save startup.cs and rebuild, hit Ctrl-F5 and once again browsing to index.html, or the root directory (as the changes above now mean the index.html file will "win" over the /home/index path when browsing the root directory.

Now the app should load correctly:

Hello Angular

Importantly, you can still browse the older ASP.NET Core MVC home page at /home/index by using the path explicitly, as below, since we'll be using these controller/action paths more as the project continues.

ASP.Net MVC home page

(4) Using ASP.NET Core Together with Angular

So far, we have two separate applications, the ASP.NET Core MVC app still at /home/index is running as before, and the Angular app launched from index.html.

Many people using ASP.NET Core and Angular JS stop here, they build the Angular website using "flat" HTML, around the index.html (in which case any web server would work), some go further and their integrations will pre-render Angular code at the server and so "bootstrap" data and code to the client, but by pre-rendering data, you now introduce another lot of code to maintain and although these can be useful in high traffic sites that change very little, they can get quite complex.

Other implementations create RESTful web services and use ASP.NET Core Web API to serve data. However, this and the previous options still leave much of ASP.NET Core's capabilities unused.

Alternately, you could use ASP.NET MVC partial views, using conventional MVC controllers and actions to execute backend C# code, use Razor mark-up in your view, and allow use of custom tag helpers and make use of ASP.NET Core server side caching - all of these together can also deliver your Angular page the HTML templates, however the templates are no longer "flat", but part of a pipeline that provides many optional touch-points.

This latter method is the technique we will use, beginning with moving the Angular home page, index.html page into the home controller and index.cshtml view, moving the common shared libraries into shared views, and creating an MVC controller that will deliver partial views and replace inline and external Angular templates.

The next steps will be to add custom tag helpers that will be used to pre-populate these Angular templates with HTML, styles and validation. Data will not be pre-loaded, instead we'll create RESTful services in ASP.NET Core to deliver data to the Angular page when requested from the client in a more conventional manner.

Angular JS quick start index.html starts as:

HTML
<!DOCTYPE html>
<html>
  <head>
    <title>Angular QuickStart</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="styles.css">

    <!-- Polyfill(s) for older browsers -->
    <script src="node_modules/core-js/client/shim.min.js"></script>

    <script src="node_modules/zone.js/dist/zone.js"></script>
    <script src="node_modules/systemjs/dist/system.src.js"></script>

    <script src="systemjs.config.js"></script>
    <script>
      System.import('app').catch(function(err){ console.error(err); });
    </script>
  </head>

  <body>
    <my-app>Loading AppComponent content here ...</my-app>
  </body>
</html>

The Angular Quickstart application has its template inline, in app.component.ts as below:

JavaScript
import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `<h1>Hello {{name}}</h1>`,
})
export class AppComponent  { name = 'Angular'; }

However, the component could also refer to an external HTML template, as shown below, assuming there was an HTML file appComponent.html present beside it:

JavaScript
import { Component } from '@angular/core';

@Component({
    selector: 'my-app',
    templateUrl: './appComponent.html'
})
export class AppComponent  { name = 'Angular'; }

We're going to use the templateUrl syntax, similar to the second example above, to point to a new controller PartialController.cs and create a view to deliver what we need.

Create the Partial Controller in the /controllers folder, ensure the file is called PartialController.cs and contains the following code:

C#
using Microsoft.AspNetCore.Mvc;
 
namespace A2SPA.Controllers
{
    public class PartialController : Controller
    {
        public IActionResult AboutComponent() => PartialView();
 
        public IActionResult AppComponent() => PartialView();
 
        public IActionResult ContactComponent() => PartialView();
 
        public IActionResult IndexComponent() => PartialView();
    }
}

This new controller will be used to deliver the HTML templates or views to our client side Angular components.

Next, create a folder called Partial in the /Viewsfolder beside /Views/Home where our new views will be put:

Partial views folder

Next, copy the three existing ASP.NET MVC files, About.cshtml, Contact.cshtml and Index.cshtml from /views/home folder to the new /views/partial folder.

Once copied, rename the copied files that are in /home/partial to AboutComponent.cshtml, ContactComponent.cshtml and IndexComponent.cshtml.

We can now clean up our homecontroller.cs, removing methods we no longer need, and to avoid missing pages or confusion in routing.

So where HomeController.cs was:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
 
namespace A2SPA.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
 
        public IActionResult About()
        {
            ViewData["Message"] = "Your application description page.";
 
            return View();
        }
 
        public IActionResult Contact()
        {
            ViewData["Message"] = "Your contact page.";
 
            return View();
        }
 
        public IActionResult Error()
        {
            return View();
        }
    }
}

Change it to be:

C#
using Microsoft.AspNetCore.Mvc;
 
namespace A2SPA.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            ViewData["Title"] = "Home";
            return View();
        }
 
        public IActionResult Error()
        {
            return View();
        }
    }
}

Normally, each ASP.NET MVC view shares a lot of common code with /views/shared/_layout.cshtml, each view (index.cshtml, about.cstml, contacts.cshtml) are shown with the content pushed inside the shared layout where it is marked with the Razor code @RenderBody() as shown in the following excerpt from_layout.cshtml:

HTML
...

    <div class="container body-content">
        @RenderBody()
        <hr />

...

In this new project, we'll still use /views/home/index and /views/shared/_layout to deliver content, however as Angular 2 takes over on the client side, the /views/partial/AppComponent.cshtml view will take on some of the tasks that were formerly the job of _layout.cshtml file, as the AppComponent.cshtml view will be used to load other Angular views.

Update /views/partial/AppComponent.cshtml to the following, to enable menu linking using Angular 2 routing:

HTML
<div class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header" (click)="setTitle('Home - A2SPA')">
            <button type="button" class="navbar-toggle" data-toggle="collapse" 
             data-target=".navbar-collapse">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a routerLink="/home" routerLinkActive="active" class="navbar-brand">A2SPA</a>
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li>
                    <a class="nav-link" (click)="setTitle('Home - A2SPA')" 
                     routerLink="/home" routerLinkActive="active">Home</a>
                </li>
                <li>
                    <a class="nav-link" (click)="setTitle('About - A2SPA')" 
                     routerLink="/about">About</a>
                </li>
                <li>
                    <a class="nav-link" (click)="setTitle('Contact - A2SPA')" 
                     routerLink="/contact">Contact</a>
                </li>
            </ul>
        </div>
    </div>
</div>
 
<div class="container body-content">
 
    <router-outlet></router-outlet>
 
    @{
        string razorServerSideData = "ASP.Net Core";
    }
 
    <hr />
    <footer>
        <p>&copy; 2017 - A2SPA = (@razorServerSideData + 
                         {{angularClientSideData}})<sup>2</sup></p>
    </footer>
</div>

Note the new Angular directive, <router-outlet> will be loading our Angular content.

Next update /views/home/index.cshtml to this:

HTML
<my-app>Loading AppComponent content here ...</my-app>

And though we will still use /views/shared/_layout.cshtml, it needs to be modified as menus are no longer handled there, but in our new AppComponent.cshtml, so replace the current contents of _layout.cshtml with this:

HTML
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - A2SPA</title>
    <base href="~/">
 
    <environment names="Development">
        <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
        <link rel="stylesheet" href="~/css/site.css" />
    </environment>
    <environment names="Staging,Production">
        <link rel="stylesheet" 
         href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/css/bootstrap.min.css"
              asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
              asp-fallback-test-class="sr-only" asp-fallback-test-property="position" 
              asp-fallback-test-value="absolute" />
        <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
    </environment>
 
</head>
<body>
 
    @RenderBody()
 
    <environment names="Development">
        <script src="~/lib/jquery/dist/jquery.js"></script>
        <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
        <script src="~/js/site.js" asp-append-version="true"></script>
        <!-- Polyfill(s) for older browsers -->
        <script src="/node_modules/core-js/client/shim.min.js"></script>
        <script src="/node_modules/zone.js/dist/zone.js"></script>
        <script src="/node_modules/systemjs/dist/system.src.js"></script>
        <script src="~/systemjs.config.js"></script>
        <script>
            System.import('app').catch(function (err) { console.error(err); });
        </script>
    </environment>
    <environment names="Staging,Production">
        <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
                asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
                asp-fallback-test="window.jQuery">
        </script>
        <script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/bootstrap.min.js"
                asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
                asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal">
        </script>
        <script src="~/js/site.min.js" asp-append-version="true"></script>
        <!-- Polyfill(s) for older browsers -->
        <script src="/node_modules/core-js/client/shim.min.js"></script>
        <script src="/node_modules/zone.js/dist/zone.js"></script>
        <script src="/node_modules/systemjs/dist/system.src.js"></script>
        <script src="~/systemjs.config.js"></script>
        <script>
            System.import('app').catch(function (err) { console.error(err); });
        </script>
    </environment>
 
    @RenderSection("scripts", required: false)
 
</body>
</html>

To test the updates, we'll need to update our app.component.ts to point to the new template driven from the partial controller, to this:

JavaScript
import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';

@Component({
    selector: 'my-app',
    templateUrl: '/partial/appComponent'
})
export class AppComponent {
    public constructor(private titleService: Title) { }
 
    angularClientSideData = 'Angular';
 
    public setTitle(newTitle: string) {
        this.titleService.setTitle(newTitle);
    }
}

The title service is a special service used to change the HTML page title added in Angular 2, as controllers can no longer be located outside the HTML <body>.

To ensure we get our about, contact and index pages working again, we need to add routing, as well as components for each.

First in the /wwwroot/app folder, create the following three components:

Create about.component.ts containing the following:

JavaScript
import { Component } from '@angular/core';
 
@Component({
    selector: 'my-about',
    templateUrl: '/partial/aboutComponent'
})
 
export class AboutComponent {
}

Next, create contact.component.ts containing the following:

JavaScript
import { Component } from '@angular/core';
 
@Component({
    selector: 'my-contact',
    templateUrl: '/partial/contactComponent'
})
 
export class ContactComponent {
}

Lastly, create index.component.ts containing the following:

JavaScript
import { Component } from '@angular/core';
 
@Component({
    selector: 'my-index',
    templateUrl: '/partial/indexComponent'
})
 
export class IndexComponent {
}

Next we add our routing logic, create the file app.routing.ts and add the following code inside it:

JavaScript
import { Routes, RouterModule } from '@angular/router';
 
import { AboutComponent } from './about.component';
import { IndexComponent } from './index.component';
import { ContactComponent } from './contact.component';
 
const appRoutes: Routes = [
    { path: '', redirectTo: 'home', pathMatch: 'full' },
    { path: 'home', component: IndexComponent, data: { title: 'Home' } },
    { path: 'about', component: AboutComponent, data: { title: 'About' } },
    { path: 'contact', component: ContactComponent, data: { title: 'Contact' } }
];
 
export const routing = RouterModule.forRoot(appRoutes);
 
export const routedComponents = [AboutComponent, IndexComponent, ContactComponent];

To include these new files, we'll update app.module.ts from this:

JavaScript
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }  from './app.component';

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

To this:

JavaScript
import { NgModule, enableProdMode } from '@angular/core';
import { BrowserModule, Title } from '@angular/platform-browser';
import { routing, routedComponents } from './app.routing';
import { APP_BASE_HREF, Location } from '@angular/common';
import { AppComponent } from './app.component';

// enableProdMode();

@NgModule({
    imports: [BrowserModule, routing],
    declarations: [AppComponent, routedComponents],
    providers: [Title, { provide: APP_BASE_HREF, useValue: '/' }],
    bootstrap: [AppComponent]
})
export class AppModule { }

We'll update our main.ts file from this:

JavaScript
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app.module';

platformBrowserDynamic().bootstrapModule(AppModule);

To allow some further logging, we use this instead:

JavaScript
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app.module';

platformBrowserDynamic().bootstrapModule(AppModule)
    .then((success: any) => console.log('App bootstrapped'))
    .catch((err: any) => console.error(err));

And now, we've completed our first ASP.NET Core + Angular 2 SPA site, delete the initial Angular 2 index.html file from /wwwroot, since we're using the ASP.NET Core /home/index view, it's no longer needed.

Finally we're ready to re-test our ASP.NET Core + Angular 2 SPA site, rebuild your project, hit Ctrl-F5 to launch your browser.

You should be able to navigate to the root directory, or /home or /home/index interchangeably and see the same thing, a page that resembles the original standard ASP.NET MVC /home/index page:

Updated Home Index view

Click on the "logo" A2SPA or the "Home" link should again show the same page, as above.

Home link from logo

Click on "About" in the menu and you should see:

About View

And click on "Contact" in the menu, we see:

Contact View

So what has changed? A close look at the copyright message at the bottom should reveal something a little different from the original:

New footer with ASPNet core Razor and Angular together

Referring back to our source, for /views/partial/AppComponent.cshtml, you see:

HTML
<div class="container body-content">
 
    <router-outlet></router-outlet>
 
    @{
        string razorServerSideData = "ASP.Net Core";
    }
 
    <hr />
    <footer>
        <p>&copy; 2017 - A2SPA = (@razorServerSideData + 
                                 {{angularClientSideData}})<sup>2</sup></p>
    </footer>
</div>

Where there are examples of Razor markup that get executed on the server, "flat" HTML that is delivered as-is, as well as an Angular 2 directive "<router-outlet>" into which the Angular templates are pushed client-side, inside the browser, and finally an example of Angular 2 data binding with {{angularClientSideData}} that is executed client side as well.

Next in Part 2 of this series, we'll look at extending these concepts to include custom tag helpers, C# code executed server-side against a data model and enable us to create form elements, or whole forms, comprising HTML, angular validation and bootstrap styling all at once, and created all in one place -where your code is "DRY" - adhering to the Don't Repeat Yourself ideal, that is without cut/paste as so often comes with SPAs of any sort.

Part 3 extends the data services, adds EF Core / entity framework and a simple SQL backend, including some sample data and then provides tag helper classes for data entry.

Part 4 will add token validation and start using Swagger tools create Angular data type definitions and services dynamically. Later articles will cover a few practical aspects of using the framework in a commerical application and how to publish to IIS.

Getting the Source

If you'd like to save time, the latest source of this is on GitHub here.

Remember this was created using Visual Studio 2015 and the latest tooling, available in January 2017.

Note: If you use Visual 2017 RC on the VS 2015 solution, it will perform a one way conversion. If there is enough interest, I'll create an article covering the VS 2017 steps, though they are very similar to those shown here.

History

  • 22nd January, 2017: Initial version

Coming soon custom tag helpers, data services and token based security.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Robert_Dyball
Software Developer (Senior)
Australia Australia
Robert works as a full stack developer in the financial services industry. He's passionate about sustainable software development, building solid software and helping to grow teams. He's had 20+ years of development experience across many areas that include scheduling, logistics, telecommunications, manufacturing, health, insurance and government regulatory and licensing sectors. Outside work he also enjoys electronics, IOT and helping a number of non-profit groups.

Comments and Discussions

 
QuestionHow lazy load or defer loading the templateUrl Pin
sinclairgf10-Dec-18 13:29
Membersinclairgf10-Dec-18 13:29 
Questiongreat tutorial how can make it work with angular cli Pin
Mohamed Refat30-Jan-18 6:01
MemberMohamed Refat30-Jan-18 6:01 
GeneralMy Vote of 5 Pin
williamdavies117-Dec-17 21:15
Memberwilliamdavies117-Dec-17 21:15 
GeneralRe: My Vote of 5 Pin
Robert_Dyball18-Dec-17 9:20
professionalRobert_Dyball18-Dec-17 9:20 
QuestionThank you Pin
kyriacos michael12-Aug-17 8:53
Memberkyriacos michael12-Aug-17 8:53 
AnswerRe: Thank you Pin
Robert_Dyball17-Aug-17 17:07
professionalRobert_Dyball17-Aug-17 17:07 
QuestionPage-Level Loading Pin
pbd21-Jun-17 8:53
Memberpbd21-Jun-17 8:53 
AnswerRe: Page-Level Loading Pin
Robert_Dyball21-Jun-17 11:20
professionalRobert_Dyball21-Jun-17 11:20 
AnswerRe: Page-Level Loading Pin
Robert_Dyball21-Jun-17 22:05
professionalRobert_Dyball21-Jun-17 22:05 
GeneralRe: Page-Level Loading Pin
pbd22-Jun-17 9:08
Memberpbd22-Jun-17 9:08 
GeneralRe: Page-Level Loading Pin
Robert_Dyball22-Jun-17 10:57
professionalRobert_Dyball22-Jun-17 10:57 
GeneralRe: Page-Level Loading Pin
pbd24-Jun-17 16:54
Memberpbd24-Jun-17 16:54 
QuestionProject does not currently work Pin
ricardo3011-Jun-17 10:34
Memberricardo3011-Jun-17 10:34 
AnswerRe: Project does not currently work Pin
Robert_Dyball12-Jun-17 23:56
professionalRobert_Dyball12-Jun-17 23:56 
QuestionLoading AppComponent content here ... Pin
Evgeny Lukiyanov11-Jun-17 7:00
MemberEvgeny Lukiyanov11-Jun-17 7:00 
AnswerRe: Loading AppComponent content here ... Pin
Robert_Dyball12-Jun-17 23:59
professionalRobert_Dyball12-Jun-17 23:59 
QuestionStuck on loading Pin
lafus Seelowe23-May-17 13:42
Memberlafus Seelowe23-May-17 13:42 
AnswerRe: Stuck on loading Pin
Robert_Dyball23-May-17 18:13
professionalRobert_Dyball23-May-17 18:13 
GeneralRe: Stuck on loading Pin
lafus Seelowe24-May-17 0:37
Memberlafus Seelowe24-May-17 0:37 
GeneralRe: Stuck on loading Pin
Robert_Dyball24-May-17 3:55
professionalRobert_Dyball24-May-17 3:55 
GeneralRe: Stuck on loading Pin
Robert_Dyball24-May-17 11:33
professionalRobert_Dyball24-May-17 11:33 
GeneralRe: Stuck on loading Pin
lafus Seelowe19-Jun-17 5:46
Memberlafus Seelowe19-Jun-17 5:46 
GeneralRe: Stuck on loading Pin
Robert_Dyball19-Jun-17 21:41
professionalRobert_Dyball19-Jun-17 21:41 
QuestionCould not get past (XHR)GET - http://localhost:50995/styles.css Pin
matthew herb19-May-17 4:29
Membermatthew herb19-May-17 4:29 
AnswerRe: Could not get past (XHR)GET - http://localhost:50995/styles.css Pin
Robert_Dyball19-May-17 11:53
professionalRobert_Dyball19-May-17 11:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.