In this tutorial we’re going to build a simple CRUD app with ASP.NET Core and Angular 2 using Visual Studio 2017. This application will manage a collection of books.
You can find the final app here. It would look like:
Prerequisites
- Visual Studio 2017 ( make sure ASP.NET and web development workload is installed)
- Node.js ( version 6 or later)
Creating the project
Microsoft.AspNetCore.SpaTemplates is a package that provides project templates for Single-Page Applications (SPAs) frameworks with ASP.NET Core. So, we need to install this package to get a project template for Angular 2.
Open the command Prompt and run the following command:
dotnet new --install Microsoft.AspNetCore.SpaTemplates::*
Once the installation is complete, you can see the SPAs templates:
Create a directory for the project:
mkdir BookStore
Change to the directory we just created:
cd BookStore
Generate an Angular 2 project template:
dotnet new angular
Install all the dependencies:
dotnet restore npm install
Open the project with Visual Studio 2017:
start BookStore.csproj
Run the app:
Preparing the project
This project is a starter seed project to get up and running with ASP.NET Core and Angular 2. It contains items that we don’t need for application. So, we’ll make a few changes to the project.
Delete the following folders:
- counter
- fetchdata
Delete SampleDataController.cs
Update the following files:
app.module.ts to:
import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { UniversalModule } from 'angular2-universal'; import { AppComponent } from './components/app/app.component' import { NavMenuComponent } from './components/navmenu/navmenu.component'; import { HomeComponent } from './components/home/home.component'; @NgModule({ bootstrap: [ AppComponent ], declarations: [ AppComponent, NavMenuComponent, HomeComponent ], imports: [ UniversalModule, RouterModule.forRoot([ { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: 'home', component: HomeComponent }, { path: '**', redirectTo: 'home' } ]) ] }) export class AppModule { }
home.component.html to:
<h1>Welcome to BookStore Application</h1>
navmenu.component.html to:
<div> <div> <div> <span>Toggle navigation</span> <span></span> <span></span> <span></span> <a>BookStore</a></div> <div></div> <div> <ul></ul> </div> </div> </div>
Run the app:
Now we’re ready to start building our application. First, we’ll create a REST API and then we’ll build the client side.
Setting up the server
We‘ll store the data in SQL server database using Entity Framework Core (EF Core) code-first approach.
Let’s create a folder named Models. Inside this folder add a Book class:
Book.cs
namespace BookStore.Models { public class Book { public int BookId { get; set; } public string Title { get; set; } public string Author { get; set; } public decimal Price { get; set; } } }
We need to install SQL Server Provider for EF Core. Open the Package Manager Console (PMC) and run:
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Create a BookStoreContext class inside the Models folder:
BookStoreContext.cs
using Microsoft.EntityFrameworkCore; namespace BookStore.Models { public class BookStoreContext : DbContext { public DbSet Books { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=BookStoreDB;Trusted_Connection=True;MultipleActiveResultSets=true"); } } }
To maintain the database you need to install EF Core Tools. Open PMC and run:
Install-Package Microsoft.EntityFrameworkCore.Tools
To generate the database, run the following commands:
Add-Migration InitialCreate update-database
To see the generated database, add a data connection in Server Explorer:
Now, it’s time to create a REST API that performs CRUD operations.
Http Verb | Route | Designation |
Get | /api/Books | Get all books |
Get | /api/Books/id | Get book by id |
Post | /api/Books | Add a new book |
Put | /api/Books/id | Update an existing book |
Delete | /api/Books/id | Delete a book |
Create a BooksController class inside the Controllers folder:
BooksController.cs
using System; using System.Net; using System.Linq; using Microsoft.AspNetCore.Mvc; using BookStore.Models; namespace BookStore.Controllers { [Route("api/[controller]")] public class BooksController : Controller { private readonly BookStoreContext _context = new BookStoreContext(); [HttpGet] public IActionResult Get() { try { var books = _context.Books.ToList(); if (books.Count > 0) return Ok(books); else return NotFound(); } catch (Exception) { return StatusCode((int)HttpStatusCode.InternalServerError); } } [HttpGet("{id}")] public IActionResult Get(int id) { try { var book = _context.Books.FirstOrDefault(e => e.BookId == id); if (book != null) return Ok(book); else return NotFound(); } catch (Exception) { return StatusCode((int)HttpStatusCode.InternalServerError); } } [HttpPost] public IActionResult Post([FromBody]Book book) { if (ModelState.IsValid == false) return BadRequest(ModelState); try { _context.Books.Add(book); _context.SaveChanges(); return Created("created", book); } catch (Exception) { return StatusCode((int)HttpStatusCode.InternalServerError); } } [HttpPut("{id}")] public IActionResult Put(int id, [FromBody]Book book) { if (id != book.BookId) return NotFound(); if (ModelState.IsValid == false) return BadRequest(ModelState); try { _context.Update(book); _context.SaveChanges(); return Ok(book); } catch (Exception) { return StatusCode((int)HttpStatusCode.InternalServerError); } } [HttpDelete("{id}")] public IActionResult Delete(int id) { try { var book = _context.Books.FirstOrDefault(e => e.BookId == id); if (book == null) return NotFound(); else { _context.Books.Remove(book); _context.SaveChanges(); return Ok(); } } catch (Exception) { return StatusCode((int)HttpStatusCode.InternalServerError); } } } }
Setting up the client
Now we’re going to create the client side that will consume the REST API we just created. It’s placed in ClientApp folder.
Create a folder called models and inside it add a book class:
book.ts
export class Book { bookId: number; title: string; author: string; price: number; }
Let’s a create a service to communicate with the back-end. Create a folder named services and inside it add a BookService class:
book.service.ts
import { Injectable } from '@angular/core'; import { Headers, Http, Response } from '@angular/http'; import 'rxjs/add/operator/toPromise'; import { Book } from "../models/book"; @Injectable() export class BookService { private _bookStoreApi = '/api/Books'; constructor(private http: Http) { } getAll(): Promise { return this.http.get(this._bookStoreApi) .toPromise() .then(response => response.json()) .catch(error => Promise.reject(error.message || error)); } get(id: Number) { return this.http.get(this._bookStoreApi + '/' + id) .toPromise() .then(response => response.json()) .catch(error => Promise.reject(error.message || error)); } save(book: Book): Promise { if (book.bookId) return this.put(book); return this.post(book); } delete(book: Book) { let headers = new Headers(); headers.append('Content-Type', 'application/json'); let url = `${this._bookStoreApi}/${book.bookId}`; return this.http .delete(url, headers) .toPromise() .catch(error => Promise.reject(error.message || error)); } private post(book: Book): Promise { return this.http .post(this._bookStoreApi, JSON.stringify(book), { headers: new Headers({ 'Content-Type': 'application/json' }) }) .toPromise() .then(response => response.json().data) .catch(error => Promise.reject(error.message || error)); } private put(book: Book) { let headers = new Headers(); headers.append('Content-Type', 'application/json'); let url = `${this._bookStoreApi}/${book.bookId}`; return this.http .put(url, JSON.stringify(book), { headers: headers }) .toPromise() .then(() => book) .catch(error => Promise.reject(error.message || error)); } }
Update app.module.ts to:
import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { UniversalModule } from 'angular2-universal'; import { AppComponent } from './components/app/app.component' import { NavMenuComponent } from './components/navmenu/navmenu.component'; import { HomeComponent } from './components/home/home.component'; import { BookService } from "./services/book.service"; @NgModule({ bootstrap: [ AppComponent ], declarations: [ AppComponent, NavMenuComponent, HomeComponent ], imports: [ UniversalModule, RouterModule.forRoot([ { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: 'home', component: HomeComponent }, { path: '**', redirectTo: 'home' } ]) ], providers: [BookService] }) export class AppModule { }
Now, we’ll add two components in our application:
- books component: displays a collection of books and allows us to view a book’s details, remove a book and navigate to book form.
- book-form component: allows adding a new book or updating a existing book.
Create a new folder inside components folder named books and add the following files:
books.component.ts
import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { BookService } from "../../services/book.service"; import { Book } from "../../models/book"; @Component({ selector: 'books', templateUrl: './books.component.html' }) export class BooksComponent implements OnInit { books: Book[]; book: Book; error: any; constructor(private router: Router, private bookService: BookService) { } ngOnInit() { this.getAll(); } getAll() { this.bookService.getAll() .then(books => this.books = books) .catch(error => this.error = error); } viewDetail(book: Book) { this.book = book; } gotoForm(id) { this.router.navigate(['/book', id]); } delete(book: Book, event: any) { event.stopPropagation(); this.bookService .delete(book) .then(response => { this.books = this.books.filter(b => b !== book); if (this.book === book) this.book = null; }) .catch(error => this.error = error); } }
books.component.html
<span>{{error}}</span> <div> <h2>Books list</h2> <div> <div> <a>Create New</a> <table> <thead> <tr> <th>Title</th> <th>Price</th> <th>Action</th> </tr> </thead> <tbody> <tr> <td>{{book.title}}</td> <td>{{book.price}}</td> <td> Edit Details Delete</td> </tr> </tbody> </table> </div> <div> <h4>Book details</h4> <div> <table> <tr> <td>Title:</td> <td>{{book.title}}</td> </tr> <tr> <td>Author:</td> <td>{{book.author}}</td> </tr> <tr> <td>Price:</td> <td>{{book.price}}</td> </tr> </table> </div> </div> </div> </div>
Inside components folder create a new folder called book-form and the following files:
book-form.component.ts
import { Component, Input, OnInit } from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; import { Book } from "../../models/book"; import { BookService } from "../../services/book.service"; @Component({ selector: 'book-form', templateUrl: './book-form.component.html' }) export class BookFormComponent implements OnInit { @Input() book: Book; updateMode = false; error: any; constructor( private bookService: BookService, private route: ActivatedRoute) { } ngOnInit() { this.route.params.forEach((params: Params) => { let id = params['id']; if (id === '') { this.updateMode = true; this.book = new Book(); } else { this.updateMode = false; this.bookService.get(id) .then(book => this.book = book); } }); } save() { this.bookService .save(this.book) .then(book => { this.book = book; this.goBack(); }) .catch(error => this.error = error); } goBack() { window.history.back(); } }
book-form.component.html
<span>{{error}}</span> <div> <div> Title: <div> </div> </div> <div> Author: <div> </div> </div> <div> Price: <div> </div> </div> <div> <div> Back Save</div> </div> </div>
Let’s define a routes associated to these components:
Update app.module.ts to:
import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { UniversalModule } from 'angular2-universal'; import { AppComponent } from './components/app/app.component' import { NavMenuComponent } from './components/navmenu/navmenu.component'; import { HomeComponent } from './components/home/home.component'; import { BooksComponent } from './components/books/books.component'; import { BookFormComponent } from './components/book-form/book-form.component'; import { BookService } from "./services/book.service"; @NgModule({ bootstrap: [ AppComponent ], declarations: [ AppComponent, NavMenuComponent, HomeComponent, BooksComponent, BookFormComponent ], imports: [ UniversalModule, FormsModule, RouterModule.forRoot([ { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: 'home', component: HomeComponent }, { path: 'books', component: BooksComponent }, { path: 'book/:id', component: BookFormComponent }, { path: '**', redirectTo: 'home' } ]) ], providers: [BookService] }) export class AppModule { }
Let’s add an item to the navigation menu that allows navigating to books components:
Update navmenu.component.html to:
<div> <div> <div> <span>Toggle navigation</span> <span></span> <span></span> <span></span> <a>BookStore</a></div> <div></div> <div> <ul> <li> <a> <span></span> Books </a></li> </ul> </div> </div> </div>
Thanks for reading!