Introduction
Prerequisites
- Visual Studio 2015: You can download it from here.
- .NET Core SDK: Download .NET Core SDK from this link.
- .NET Framework 4.5.2: Download .NET Framework 4.5.2 from this link.
- .NodeJS: Download NodeJS from this link.
- Source code: Download source from this link.
In this part, we will see in detail how to:
- Create a student component
- Communicate with Server via Web API
- Create Form and pushing data to server
- Authentication using AspNetCore
CookieAuthenticationOptions
Background
Before reading this part, you should see Part 1.
Using the Code
Let's rock!
Add student
components:
- wwwroot/app/components/student/student.html
- wwwroot/app/components/student/student.controller.html
Content of student.html:
<h2>Student List</h2>
<table class="table table-striped">
<thead>
<tr>
<th>Firstname</th>
<th>Lastname</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<tr>
<td>John</td>
<td>Doe</td>
<td>john@example.com</td>
</tr>
<tr>
<td>Mary</td>
<td>Moe</td>
<td>mary@example.com</td>
</tr>
<tr>
<td>July</td>
<td>Dooley</td>
<td>july@example.com</td>
</tr>
</tbody>
</table>
Content of student.controller.js:
(function () {
'use strict';
angular
.module('app')
.controller('StudentController', MainController);
function MainController($scope) {
var vm = this;
}
})();
Modify main.html into:
<h2>This application consists of:</h2>
<ul>
<li>Sample pages using ASP.NET Core with
AngularJS, NPM, Gulp, Bower, Bootstrap, Jquery</li>
<li>More detail <a href="http://www.codeproject.com/script/Articles/
ArticleVersion.aspx?waid=209236&aid=1102877">here</a></li>
</ul>
Modify index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AspNetCoreSPA</title>
<script type="text/javascript">
var site = site || {};
</script>
<!--
<!--
<!--
<!--
</head>
<body ng-app="app">
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<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>
</button>
<a ui-sref="home" class="navbar-brand">AspNetCoreSPA</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a ui-sref="home">Home</a></li>
<li><a ui-sref="student">Student</a></li>
</ul>
</div>
</div>
</div>
<div class="container body-content">
<div class="jumbotron">
<h1>AspNetCoreSPA</h1>
<p class="lead">Welcome to AspNetCoreSPA</p>
</div>
<div class="row">
<div ui-view></div>
</div>
<hr />
<footer>
<p>© 2016 - Toan Manh Nguyen</p>
</footer>
</div>
<!--
<!--
<!--
<!--
</body>
</html>
Explanation about Routing:
ui-sref="home"
will map with:
$stateProvider
.state('home', {
url: '/',
templateUrl: 'app/main/main.html',
controller: 'MainController',
controllerAs: 'mainCtrl'
})
Full content of index.route.js:
(function () {
'use strict';
angular
.module('app')
.config(routerConfig);
routerConfig.$inject = ['$stateProvider', '$urlRouterProvider'];
function routerConfig($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/');
$stateProvider
.state('home', {
url: '/',
templateUrl: 'app/main/main.html',
controller: 'MainController',
controllerAs: 'mainCtrl'
})
.state('student', {
url: '/student',
templateUrl: 'app/components/student/student.html',
controller: 'StudentController',
controllerAs: 'studentCtrl'
});
}
})();
Now hit F5 and see the new result:
Click Student
menu:
The same result of Part 1 but it have some navigation, now we will get student list from server via http and load the result in student.html.
Firstly, create StudentController
:
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
namespace AspNetCoreSPA.Web.Controllers
{
[Route("api/student")]
public class StudentController : Controller
{
private List<Student> _students = new List<Student>
{
new Student { FirstName = "John", LastName = "Doe", Email = "john@example.com"},
new Student { FirstName = "Mary", LastName = "Moe", Email = "mary@example.com"},
new Student { FirstName = "July", LastName = "Dooley", Email = "july@example.com"}
};
[Route("getAll")]
public IEnumerable<Student> GetAll()
{
return _students;
}
}
public class Student
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
}
We have GetAll
method, this method returns list of student
(JSON formatted).
Modify student.html:
<h2>Student List</h2>
<table class="table table-striped">
<thead>
<tr>
<th>Firstname</th>
<th>Lastname</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="student in studentCtrl.students">
<td>{{student.FirstName}}</td>
<td>{{student.LastName}}</td>
<td>{{student.Email}}</td>
</tr>
</tbody>
</table>
Did you see "studentCtrl.students
", if we studied Angular before, it must have ng-controller="StudentController
as studentCtrl
" right? But take a look at index.route.js:
.state('student', {
url: '/student',
templateUrl: 'app/components/student/student.html',
controller: 'StudentController',
controllerAs: 'studentCtrl'
);
Key point is here: controller
and controllerAs
, it's the same as ng-controller
.
Next, we modify student.controller.js:
(function () {
'use strict';
angular
.module('app')
.controller('StudentController', StudentController);
StudentController.$inject = ['$http', '$http'];
function StudentController($scope, $http) {
var vm = this;
vm.students = [];
$http.get("api/student/getAll").then(function(response)
{ vm.students = response.data; });
}
})();
Hit F5, click Student menu and see the result:
Exactly the same before!
Next, we will create a Create button and create a new student
.
Before doing that, add those codes in index.module.js: This automatically clears the cache whenever the ng-view
content changes.
angular.module('app').run(function ($rootScope, $templateCache) {
$rootScope.$on('$viewContentLoaded', function () {
$templateCache.removeAll();
});
});
Modify student.html:
<h2>Student List</h2>
<table class="table table-striped">
<thead>
<tr>
<th>Firstname</th>
<th>Lastname</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="student in studentCtrl.students">
<td>{{student.FirstName}}</td>
<td>{{student.LastName}}</td>
<td>{{student.Email}}</td>
</tr>
</tbody>
</table>
<input type="button" class="btn btn-default" value="Create"
data-toggle="modal" data-target="#formCreateStudent"/>
<form id="formCreateStudent" class="modal fade" role="dialog">
<div class="modal-dialog">
<!--
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close"
data-dismiss="modal">×</button>
<h4 class="modal-title">Create new student</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label for="firstName">First Name</label>
<input type="text" class="form-control" id="firstName"
placeholder="First Name"
ng-model="studentCtrl.createStudentInput.FirstName">
</div>
<div class="form-group">
<label for="lastName">Last Name</label>
<input type="text" class="form-control" id="lastName"
placeholder="Last Name"
ng-model="studentCtrl.createStudentInput.LastName">
</div>
<div class="form-group">
<label for="emailAddress">Email address</label>
<input type="email" class="form-control" id="emailAddress"
placeholder="Email" ng-model="studentCtrl.createStudentInput.Email">
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-success"
ng-click="studentCtrl.createStudent()">Submit</button>
<button type="button" class="btn btn-default"
data-dismiss="modal">Close</button>
</div>
</div>
</div>
</form>
Note that we use bootstrap modal to visible dialog.
Modify student.controller.js:
(function () {
'use strict';
angular
.module('app')
.controller('StudentController', StudentController);
StudentController.$inject = ['$scope', '$http'];
function StudentController($scope, $http) {
var vm = this;
vm.students = [];
vm.createStudentInput = {};
vm.createStudent = function () {
$http.post("api/student/createStudent", JSON.stringify(vm.createStudentInput))
.then(function (response) {
vm.students = response.data;
$('#formCreateStudent').modal('toggle');
});
}
$http.get("api/student/getAll")
.then(function(response) {
vm.students = response.data;
});
}
})();
Explanation:
Let's see StudentController
now:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
namespace AspNetCoreSPA.Web.Controllers
{
[Route("api/student")]
public class StudentController : Controller
{
private static List<Student> _students = new List<Student>
{
new Student { FirstName = "John", LastName = "Doe", Email = "john@example.com"},
new Student { FirstName = "Mary", LastName = "Moe", Email = "mary@example.com"},
new Student { FirstName = "July", LastName = "Dooley", Email = "july@example.com"}
};
[Route("getAll")]
[HttpGet]
public IActionResult GetAll()
{
return Json(_students);
}
[Route("createStudent")]
[HttpPost]
public IActionResult CreateStudent([FromBody] Student student)
{
if (!ModelState.IsValid)
{
return BadRequest();
}
_students.Add(student);
return Json(_students);
}
}
public class Student
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
[Required]
public string Email { get; set; }
}
}
In AspNetCore, ApiController
and Controller
were merged into one called "Controller
", with request mapping model we should include [FromBody]
for mapping data to our model.
After all, hit F5 and see the result!
Step 1: Press Create button and fill in data:
Step 2: Press submit button and see the result, new record added.
Authentication using CookieAuthenticationOptions
Context
- Any user must be authenticated before joining our system
- User is authenticated by Username + Password
- After login successful, user can call our APIs
- Screen transition: Login -> Main screen -> Child screen. If login fails or cookie timeout, transfer back to Login screen.
Implementation
- Client side:
Index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AspNetCoreSPA</title>
<script type="text/javascript">
var site = site || {};
</script>
<!--
<!--
<!--
<!--
</head>
<body ng-app="app">
<div ui-view="login"></div>
<div ui-view="main"></div>
<!--
<!--
<!--
<!--
</body>
</html>
<div ui-view="login"></div>
: Login.html will be rendered here.
<div ui-view="main"></div>
: Main.html will be rendered here (Main.html like _Layout.cshtml)
Let's see the main parts:
main.html
: As I said before, this page is our Master page, all child pages will be rendered:
<div ui-view="content"></div>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<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>
</button>
<a ui-sref="home" class="navbar-brand">AspNetCoreSPA</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a ui-sref="main">Home</a></li>
<li><a ui-sref="student">Student</a></li>
</ul>
</div>
</div>
</div>
<div class="container body-content">
<div class="jumbotron">
<h1>AspNetCoreSPA</h1>
<p class="lead">Welcome to AspNetCoreSPA</p>
</div>
<div ui-view="content"></div>
<hr />
<footer>
<p>© 2016 - Toan Manh Nguyen</p>
</footer>
</div>
main.controller.js:
(function () {
'use strict';
angular
.module('app')
.controller('MainController', MainController);
MainController.$inject = ['$scope'];
function MainController($scope) {
var vm = this;
}
})();
main.route.js: Refer here for Nested States and Nested Views for the convention "content@main
".
(function () {
'use strict';
angular
.module('app')
.config(routerConfig);
routerConfig.$inject = ['$stateProvider'];
function routerConfig($stateProvider) {
$stateProvider
.state('main',
{
url: '/',
views: {
'main': {
templateUrl: 'app/main/main.html',
controller: 'MainController',
controllerAs: 'vm'
},
'content@main': {
templateUrl: 'app/components/home/home.html',
controller: 'HomeController',
controllerAs: 'vm'
}
}
});
}
})();
Back to old ASP.NET MVC, the concept is: _Layout.cshtml + Home.cshtml (We use layout for master page and the first page is home). Now in Angular js, the concept is similar: We always render home.html in main.html at the first time.
The login parts:
login.html
<div class="container">
<div class="row">
<div class="col-sm-6 col-md-4 col-md-offset-4">
<h1 class="text-center login-title">Login to our System</h1>
<div class="account-wall">
<form class="form-signin">
<input type="text" ng-model="vm.loginInfo.UserName"
class="form-control" placeholder="Username" required autofocus>
<input type="password" ng-model="vm.loginInfo.Password"
class="form-control" placeholder="Password" required>
<input class="btn btn-lg btn-primary btn-block"
type="button" ng-click="vm.login()" value="Sign in" />
</form>
</div>
</div>
</div>
</div>
login.controller.js:
(function () {
'use strict';
angular
.module('app')
.controller('LoginController', LoginController);
LoginController.$inject = ['$scope', '$http', '$state', 'auth0Service'];
function LoginController($scope, $http, $state, auth0Service) {
var vm = this;
vm.loginInfo = {
UserName: "test01",
Password: "Qwer!@#12345"
};
vm.login = function () {
auth0Service.login(vm.loginInfo, function (response) {
if (response == "OK") {
auth0Service.authenticate();
$state.go('main');
}
});
}
}
})();
Notice that we will transfer to "main
" page when the login status is "OK
": $state.go('main');
login.route.js:
(function () {
'use strict';
angular
.module('app')
.config(routerConfig);
routerConfig.$inject = ['$stateProvider'];
function routerConfig($stateProvider) {
$stateProvider
.state('login', {
url: '/login',
views: {
'login@': {
templateUrl:'app/login/login.html',
controller: 'LoginController',
controllerAs: 'vm'
}
}
});
}
})();
But if status is 401??? What will happen?
Let's see site.ng.js:
'responseError': function (ngError) {
var state = $injector.get('$state');
var auth0 = $injector.get('auth0Service');
var error = {
message: ngError.data ||
site.ng.http.defaultError.message,
details: ngError.statusText ||
site.ng.http.defaultError.details,
responseError: true
}
if (ngError.status === 401) {
auth0.clear();
state.go("login");
} else {
site.ng.http.showError(error);
}
return $q.reject(ngError);
}
We check if the status is 401 -> transfer back to login page.
- Server side:
Create "MyIdentity
" class:
using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace AspNetCoreSPA.Web.Configurations
{
public static class MyIdentity
{
public static void UseMyIdentity(this IApplicationBuilder app)
{
var applicationCookie = new CookieAuthenticationOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
CookieName = "AspNetCoreSPA",
Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync,
OnRedirectToLogin = ctx =>
{
if (ctx.Request.Path.StartsWithSegments("/api") &&
ctx.Response.StatusCode == (int)HttpStatusCode.OK)
{
ctx.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
else
{
ctx.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return Task.FromResult(0);
}
},
ExpireTimeSpan = TimeSpan.FromHours(1),
CookieHttpOnly = true
};
IdentityOptions identityOptions =
app.ApplicationServices.GetRequiredService
<IOptions<IdentityOptions>>().Value;
identityOptions.Cookies.ApplicationCookie = applicationCookie;
app.UseCookieAuthentication(identityOptions.Cookies.ExternalCookie);
app.UseCookieAuthentication(identityOptions.Cookies.ApplicationCookie);
}
}
}
In Startup.cs, add:
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseStaticFiles();
app.UseMyIdentity();
app.UseMvcWithDefaultRoute();
}
From now, if you want to secure any controller or action, just use [Authorize]
attribute:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace AspNetCoreSPA.Web.Controllers
{
[Produces("application/json")]
[Route("api/student")]
[Authorize]
public class StudentController : Controller
{
private static List<Student> _students = new List<Student>
{
new Student
{ FirstName = "John", LastName = "Doe", Email = "john@example.com"},
new Student
{ FirstName = "Mary", LastName = "Moe", Email = "mary@example.com"},
new Student
{ FirstName = "July", LastName = "Dooley", Email = "july@example.com"}
};
[Route("getAll"), HttpGet]
public IActionResult GetAll()
{
return Json(_students);
}
[Route("createStudent"), HttpPost]
public IActionResult CreateStudent([FromBody] Student student)
{
if (!ModelState.IsValid)
{
return BadRequest();
}
_students.Add(student);
return Json(_students);
}
[Route("searchStudent"), HttpGet]
public IActionResult Search([FromQuery] string firstName)
{
return Json(_students.Where
(student => student.FirstName.Equals(firstName)));
}
}
public class Student
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
[Required]
public string Email { get; set; }
}
}
History
- 2016/05/29 - Created Part 2
- 2016/06/04 - Updated authentication using
CookieAuthenticationOptions