65.9K
CodeProject is changing. Read more.
Home

ASP.NET Core RC2 Using WEB API And AngularJS - Part 2

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (13 votes)

May 29, 2016

CPOL

3 min read

viewsIcon

32795

In this article, you will learn about ASP.NET Core RC2 using WEB API and AngularJS.

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>

    <!-- bower:css -->
    <!-- endbower -->
    <!-- inject:css -->
    <!-- endinject -->
</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>&copy; 2016 - Toan Manh Nguyen</p>
        </footer>
    </div>

    <!-- bower:js -->
    <!-- endbower -->
    <!-- inject:js -->
    <!-- endinject -->
</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">
        <!-- Modal content-->
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" 
                data-dismiss="modal">&times;</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) {
                    // Re-load data
                    vm.students = response.data;

                    // Close dialog
                    $('#formCreateStudent').modal('toggle');
                });
        }

        $http.get("api/student/getAll")
            .then(function(response) {
                vm.students = response.data;
            });
    }
})();

Explanation:

  • We use $http for get and post requests.
  • vm.createStudent will be called when we click Submit button in Create form.
  • $http.post or $http.get is a shortcut method (more details here).
  • After student has been created, we also need to reload data and close modal.
  • vm.students = response.data;
    
    $('#formCreateStudent').modal('toggle');

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

  1. 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>
    
        <!-- bower:css -->
        <!-- endbower -->
        <!-- inject:css -->
        <!-- endinject -->
    </head>
    <body ng-app="app">
        <div ui-view="login"></div>
        <div ui-view="main"></div>
    
        <!-- bower:js -->
        <!-- endbower -->
        <!-- inject:js -->
        <!-- endinject -->
    </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>&copy; 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.

  2. 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 request comming from web api
                            // always return Unauthorized (401)
                            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);
                        }
                    },
    
                    // Set to 1 hour
                    ExpireTimeSpan = TimeSpan.FromHours(1),
    
                    // Notice that if value = false, 
                    // you can use angular-cookies ($cookies.get) to get value of Cookie
                    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