The source code for this project is available at GitHub and here's a live example of the website.
This second part of the series is focusing on implementing the Angular 2 tutorial "Tour of Heroes" app. I will explain where changes are required from the original Angular 2 tutorial to get it running in Asp.Net Core with Visual Studio 2015 2017.
Right now, it's on .Net Core 1.0.0 1.0.1 (You should upgrade!) 1.1.0 1.1.2 2.0 and Angular 2 RC3 RC4 RC5 RC6 Final 4.1.3 5.2.3, but I will write about migrations when there are updates. You can read about what I had to change for each migration in the commits on GitHub.
Server Side - Implement the Heroes Api Controller with an InMemory Database
For this example application, I'm going to use EntityFrameworkCore's InMemory database provider for handling the heroes' data. To implement this, add "Microsoft.EntityFrameworkCore.InMemory": "1.1.0" to your project.json. Then, create a new folder Data in your project directory with two classes:
HeroesContext.cs
using Microsoft.EntityFrameworkCore; namespace NetCoreHeroes.Data { public class HeroesContext : DbContext { public DbSet<Hero> Heroes { get; set; } public HeroesContext(DbContextOptions options): base(options) { } } }
Hero.cs
namespace NetCoreHeroes.Data { public class Hero { public int Id { get; set; } public string Name { get; set; } } public class HeroPost { public string Name { get; set; } } }
Please note that I'm using neither a repository nor any data transfer objects to keep the example code very short and simple.
Make the context available to the DI provider by adding the following code to your Startups ConfigureServices method:
services.AddEntityFrameworkInMemoryDatabase() .AddDbContext<HeroesContext>(options => options.UseInMemoryDatabase());
Finally, rename the ValuesController that was generated by Visual Studios project template to HeroesController and implement it as follows:
using System.Linq; using Microsoft.AspNetCore.Mvc; using NetCoreHeroes.Data; namespace NetCoreHeroes.Controllers { [Route("api/[controller]")] public class HeroesController : Controller { private readonly HeroesContext _context; public HeroesController(HeroesContext context) { _context = context; } [HttpGet] public IActionResult Get() { var heroes = _context.Heroes; return Ok(heroes); } [HttpGet("{id}")] public IActionResult Get(int id) { var hero = _context.Heroes .FirstOrDefault(dbHero => dbHero.Id == id); if (hero == null) { return NotFound(); } return Ok(hero); } [HttpPut("{id}")] public IActionResult Put(int id, [FromBody] Hero updatedHero) { if (updatedHero == null || id != updatedHero.Id) { return BadRequest(); } var hero = _context.Heroes.FirstOrDefault(dbHero => dbHero.Id == updatedHero.Id); if (hero == null) { return NotFound(); } hero.Name = updatedHero.Name; _context.SaveChanges(); return Ok(); } [HttpPost] public IActionResult Post([FromBody]Hero newHero) { if (string.IsNullOrWhiteSpace(newHero.Name)) { return BadRequest(new {error = "Hero must have a name!"}); } var newDbHero = new Hero { Name = newHero.Name }; _context.Heroes.Add(newDbHero); _context.SaveChanges(); return Ok(newDbHero); } [HttpDelete("{id}")] public IActionResult Delete(int id) { var hero = _context.Heroes.FirstOrDefault(dbHero => dbHero.Id == id); if (hero == null) { return NotFound(); } _context.Heroes.Remove(hero); _context.SaveChanges(); return Ok(); } } }
And finally, add this snippet to your Startups Configure method:
using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope()) { using (var dbContext = serviceScope.ServiceProvider.GetService<HeroesContext>()) { var heroNames = new[] {"Mr. Nice", "Narco", "Bombasto", "Celeritas", "Magneta", "RubberMan", "Dynama", "Dr IQ", "Magma", "Tornado"}; foreach (var heroName in heroNames) { dbContext.Heroes.Add(new Hero {Name = heroName}); } dbContext.SaveChanges(); } }
Add the UseStaticFiles Middleware
Since you'll be serving static files from the server, add the "Microsoft.AspNetCore.StaticFiles": "1.1.0" dependency to your project.json and then enable the middleware in your project Startup class' Configure method via app.UseStaticFiles().
Now your server's ready to be used in the Tour of Heroes!
Client Side - Implement the Angular 2 Tutorial
There's very little change required to the client side Angular 2 components. Your Index.cshtml is basically the same as in the original tutorial with the exception that node_modules/ is being replaced by lib/ and there's an additional <base /> tag added so the application base is correctly set in case you're not at the root of the website with your Angular 2 app. Here's what your Index.cshtml should look like:
<!DOCTYPE html> <html> <head> <title>NetCoreHeroes</title> <base href="@ViewData["AngularBase"]" /> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="~/app/styles.css"> <!-- 1. Load libraries --> <!-- Polyfill(s) for older browsers --> <script src="lib/core-js/client/shim.min.js"></script> <script src="lib/zone.js/dist/zone.js"></script> <script src="lib/reflect-metadata/Reflect.js"></script> <script src="lib/systemjs/dist/system.src.js"></script> <!-- 2. Configure SystemJS --> <script src="systemjs.config.js"></script> <script> System.import('app').catch(function(err){ console.error(err); }); </script> </head> <body> <my-app>Loading...</my-app> </body> </html>
Changes to Angular 2 Components
Regarding the whole tutorial, you need to change the following:
Delete in-memory-data.service.ts and the associated components.
We're using a .Net Core web api backend, so no need to mock the data.
Remove imports and providers for the in memory web api in app.module.ts
It should finally look like this:
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { HttpModule } from '@angular/http'; import { FormsModule } from '@angular/forms'; @NgModule({ imports: [ BrowserModule, HttpModule, FormsModule ], declarations: [AppComponent], bootstrap: [AppComponent] }) export class AppModule { }
Modify hero.service.ts to point to the correct Url for the HeroesController (api/heroes instead of app/heroes) and change wherever you're accessing the response object via response.json().data to just response.json(). Our web api does return the objects directly without putting another container with a data property around them. For example:
getHeroes(): Promise<Hero[]> { return this.http.get(this.heroesUrl) .toPromise() .then(response => response.json().data) .catch(this.handleError); }
becomes this
getHeroes(): Promise<Hero[]> { return this.http.get(this.heroesUrl) .toPromise() .then(response => response.json()) .catch(this.handleError); }
Congratulations, you've now got the Tour of Heroes running in Asp.Net Core!
Update 30.08.2016: Updated Angular 2 to RC5
Update 04.09.2016: Updated Angular 2 to RC6
Update 22.09.2016: Updated Angular 2 to 2.0.0 Final and Asp.Net Core to 1.0.1
Update 23.11.2016: Updated Asp.Net Core to 1.1.0
Update 21.02.2017: The demo project is now using webpack as module loader, see here (and on GitHub) how to upgrade
Update 28.05.2017: Updated Angular to 4.1.3 and Asp.Net Core to 1.1.2. Switched to Visual Studio 2017 csproj format
Update 03.02.2018: Updated Angular to 5.2.3 and Asp.Net Core to 2.0