Using ASP.NET 5 and EF7 with Modern Web Dev Tools like AngularJS and Grunt

Posted by: Ravi Kiran , on 5/8/2015, in Category ASP.NET MVC
Views: 52664
Abstract: ASP.NET 5 has been designed to make it simpler to work with some of the coolest modern web development tools like AngularJS and Grunt. This article uses these technologies to create an end-to-end Virtual Tutorial Tracker application.

The web development ecosystem is changing rapidly. The good thing is, existing frameworks are catching up on the latest and greatest trends and making them first class citizens in their frameworks.

 

In recent times, the Microsoft’s web development stack, ASP.NET, has started making several changes to itself including:

  • Moving from XML to JSON for storing configurations
  • Out-of-the-box Dependency Injection
  • Leveraging Node.js environment for packaging client side dependencies
  • Reducing the start time of application by removing all dependencies
  • Providing an easier way to build and plug middleware
  • Unifying ASP.NET MVC, Web API and Web Pages, into a single programming model called MVC 6
  • Build and run cross-platform web apps on OS X and Linux with the Mono runtime
  • Ability to host on IIS or self-host in your own process


 

This article is published from the DotNetCurry .NET Magazine – A Free High Quality Digital Magazine for .NET professionals published once every two months. Subscribe to this eMagazine for Free and get access to hundreds of free .NET tutorials from experts

Because of these changes, ASP.NET is becoming an easier ecosystem for building and running rich front-end based applications on Windows as well OS X and Linux. Because of availability of task runners like Grunt and Gulp and the ability to use the most frequently updated package sources like NPM and Bower, we need not wait for the package to be available or, updated on NuGet. So it is a win-win situation for framework and the developers using the framework.

In this article, we will see how to build a simple Virtual Tutorial Tracker application using ASP.NET 5 and AngularJS. The list of technologies used in this article are:

  • SQL Server
  • ASP.NET 5
  • Entity Framework
  • AngularJS
  • Grunt

Setting up the ASP.NET Project

For this article, I have used Visual Studio 2015 CTP. You can also use the Free Visual Studio 2013 Community Edition to build the sample. Open Visual Studio 2013 or, 2015 and select File -> New Project. From the new Project dialog box, choose the option of ASP.NET Web Application and from the ASP.NET project dialog, choose ASP.NET Preview Starter Web and hit OK.

aspnet-new-project

This template creates an ASP.NET 5 application with most of the required hooks. It contains a Bootstrap-based ASP.NET application with Identity and a DbContext to create database to store Identity information. It uses Bower to bring in jQuery and bootstrap and includes a gruntfile with a few tasks. The package.json file contains the NPM tasks required for Grunt. We will add more content to these files.

Adding Bower components and Grunt tasks

As already mentioned, we will be using AngularJS in this application. Let’s add an entry of the required Angular packages to the bower.json file. Open the bower.json file and modify the list of dependencies to the following:

"dependencies": {
    "bootstrap": "3.0.0",
    "jquery": "1.10.2",
    "jquery-validation": "1.11.1",
    "jquery-validation-unobtrusive": "3.2.2",
    "hammer.js": "2.0.4",
    "bootstrap-touch-carousel": "0.8.0",
    "angular": "1.3.15",
    "angular-route": "1.3.15"
}

We need to add a couple of grunt tasks to copy the static files to their target locations and minify them. Let’s add these packages to the package.json file. Add the following packages to the devDependencies section in package.json file:

"devDependencies": {
  "grunt": "0.4.5",
  "grunt-bower-task": "0.4.0",
  "grunt-bowercopy": "^1.2.0",
  "grunt-contrib-copy": "^0.8.0",
  "grunt-contrib-concat": "^0.5.1",
  "grunt-html2js": "^0.3.1"
}

Add a new folder to the project and change the name to App. Add two folders named Scripts and Templates to this folder. We will add our JavaScript and HTML template files to these respective folders. The ASP.NET Starter application contains a folder named wwwroot; this folder is used by the project to store the static files that have to be deployed. So we need to copy all required libraries and custom scripts into this folder. Which means, this is the target folder for the Grunt tasks.

Open the file gruntfile.js and replace the code in it with the following:

module.exports = function (grunt) {
    grunt.initConfig({
        bowercopy: {
            options: {
                runBower: true,
                destPrefix: 'wwwroot/lib'
            },
            libs: {
                files: {
                    'angular': 'angular',
                    'angular-route': 'angular-route',
                    'jquery': 'jquery',
                    'bootstrap': 'bootstrap/dist/css'
                }
            }
        },
        copy: {
            main: {
                expand: true,
                flatten: true,
                filter: 'isFile',
                src: ['App/Scripts/*.js'],
                dest: 'wwwroot/dist/'
            }
        },
        concat: {
            options: {
                separator: ';'
            },
            dist: {
                src: ['wwwroot/dist/*.js'],
                dest: 'wwwroot/dist/appCombined.js'
            }
        },
        html2js: {
            options: {
                module: 'virtualTrainingTemplates'
            },
            main: {
                src: ['App/Templates/*.html'],
                dest: 'wwwroot/dist/templates.js'
            }
        }
    });
 
    // This command registers the default task which will install bower packages into wwwroot/lib
    grunt.registerTask("default", ["bowercopy:libs", "html2js", "copy:main", "concat"]);
 
    // The following line loads the grunt plugins.
    // This line needs to be at the end of this  file.
    grunt.loadNpmTasks("grunt-bower-task");
    grunt.loadNpmTasks("grunt-bowercopy");
    grunt.loadNpmTasks("grunt-contrib-copy");
    grunt.loadNpmTasks("grunt-contrib-concat");
    grunt.loadNpmTasks("grunt-html2js");
};

There are 4 tasks configured in this file:

  • bowercopy: To install the front-end libraries and copy them into the wwwroot folder
  • copy: To copy custom scripts into a folder called dist inside the wwwroot folder
  • html2js: To convert HTML templates to AngularJS modules
  • concat: To concatenate all JavaScript files into a single file
  • default: It is a custom alias task that runs the above tasks in an order

Visual Studio has built-in tools to install the NPM packages and run grunt tasks. Check my article on Using Grunt, Gulp and Bower in Visual Studio 2013 and 2015 to learn more about it.

Open the Task Runner Explorer from View -> Other Windows -> Task Runner Explorer menu option. The widget reads gruntfile and lists all tasks to run them easily. We can set the option to run the Grunt tasks on every build. To do so, you need to right click on the task and choose bindings and then your favorite option from the menu. Following screenshot sets grunt to run after every build:

grunt-task

Setting up Database Using Entity Framework

The project already includes a beta version of Entity Framework 7 and uses it to create a database to hold the Identity tables. The DbContext class of the project is ApplicationDbContext and it can be found in the IdentityModels.cs file inside the Models folder of the project. We will add the DbSet properties of the new tables to the same class. This context is already added to the services in ConfigureServices method of the Startup class, so we don’t need to register the DbContext class.

In the Models folder, add a new file and name it Course.cs and add the following code to it:

public class Course
{
  public int CourseID { get; set; }
  public string Title { get; set; }
  public int Duration { get; set; }
  public string Author { get; set; }
  public virtual ICollection Trackers { get; set; } }

Add another file and name it CourseTracker.cs and add the following code to it:

public class CourseTracker
{
  public int CourseTrackerID { get; set; }
  public int CourseID { get; set; }
  public string ApplicationUserId { get; set; }
  public bool IsCompleted { get; set; }
  public virtual Course Course { get; set; }
  public virtual ApplicationUser ApplicationUser { get; set; }
}

Now we need to add the DbSet entries to the ApplicationDbContext class.

public DbSet Courses { get; set; } public DbSet CourseTrackers { get; set; } 

The project comes with pre-configured EF Code First migrations. But as we added new classes to the DbContext, let’s create a new migration. Delete the existing Migrations folder. Open Package Manager Console and move to the folder where the project.json file is located. In this folder, run the following commands in the command prompt:

> k ef migration add init

> k ef migration apply

Once these commands are executed, you would see the database with the tables to store Identity data and the two tables corresponding to the entities we added.

As of now, Entity Framework 7 doesn’t support seeding data. The only way to seed data is using the Configure method of the Startup class. This method is executed at the beginning of the application, so we can insert the initial data in this method.

The arguments passed to this method are injected by the default Dependency Injection system that comes with ASP.NET 5. So we can specify a parameter for the DbContext object and it will be available to us. Following snippet adds some rows to the Courses table.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerfactory, ApplicationDbContext context)
{
  //Rest of logic inside the method

  if (context.Courses.FirstOrDefault() == null)
  {
    context.Courses.Add(new Course() { Title = "Basics of ASP.NET 5", Author = "Mahesh", Duration = 4 });
    context.Courses.Add(new Course() { Title = "Getting started with Grunt", Author = "Ravi", Duration = 3 });
    context.Courses.Add(new Course() { Title = "What's new in Angular 1.4", Author = "Ravi", Duration = 4 });
    context.Courses.Add(new Course() { Title = "Imagining JavaScript in ES6", Author = "Suprotim", Duration = 4 });
    context.Courses.Add(new Course() { Title = "C# Best Practices", Author = "Craig", Duration = 3 });

    context.SaveChanges();
  }
}

If you run the application now and then check the database, you will see the new data inserted into the database.

Designing REST APIs using ASP.NET Web API

Unlike previous versions of ASP.NET, the base controllers for both MVC controller and Web API controller are now the same. But we should take the responsibility of not combining both behaviors in one controller. It is always advised to keep the controller serving views, and the controller serving JSON data; in separate classes.

The Configure method of Startup class adds MVC to the services, the same service can serve Web API as well. Default settings of the Web API return JSON response in Pascal case notation. As we prefer Camel-case notation in JavaScript, let’s change the output formatter to serialize JSON data into Camel-case notation. Following code does this:

services.AddMvc().Configure(options => {   int position = options.OutputFormatters.FindIndex(f => f.Instance is JsonOutputFormatter);    var settings = new JsonSerializerSettings()   {     ContractResolver = new CamelCasePropertyNamesContractResolver()   };   var formatter = new JsonOutputFormatter();   formatter.SerializerSettings = settings;    options.OutputFormatters.Insert(position, formatter); });  

REST API for Courses

Let’s write a REST API to serve data in the Courses table. The controller will have a single GET endpoint to serve list of courses. I want this method to respond based on user’s identity. Following are the two kinds of responses:

1. If the user is logged in, return courses along with a course track object

2. If the request is by an anonymous user, return data in the courses table with the course track object set to null

As the schema of the data to be returned from this API doesn’t match with the Course class, we need a Data Transfer Object (DTO). Following is the DTO class:

public class CourseDTO
{
  public string Author { get; internal set; }
  public int CourseId { get; internal set; }
  public int Duration { get; internal set; }
  public string Title { get; internal set; }
  public CourseTracker Tracked { get; internal set; }
}

As the data conversion needs some logic, it is better to have a repository class for data conversion. Add a new class to the Models folder and name it Courserepository. Following is the logic inside this class:

public class CourseRepository
{
  ApplicationDbContext _context;

  public CourseRepository(ApplicationDbContext context)
  {
    this._context = context;
  }

  public IEnumerable GetCourses(ClaimsIdentity identity)   {     if (identity.IsAuthenticated)     {       var userId = identity.Claims.FirstOrDefault().Value;        var userTrackers = _context.CourseTrackers.Where(ct => ct.ApplicationUserId == userId);        var withTrackStatus = this._context.Courses.Select(c => new CourseDTO()       {         Tracked = userTrackers.FirstOrDefault(t => t.CourseID == c.CourseID),         CourseId = c.CourseID,         Author = c.Author,         Title = c.Title,         Duration = c.Duration       });        return withTrackStatus.ToList();     }      return this._context.Courses.Select(c => new CourseDTO()     {       Tracked = null,       CourseId = c.CourseID,       Author = c.Author,       Title = c.Title,       Duration = c.Duration     }).ToList();   } }  

To be able to inject the repository class to the controller, we need to add one statement to the ConfigureService method of Startup class.

services.AddTransient(typeof(CourseRepository), typeof(CourseRepository));

Now add a new MVC Controller to the Controllers folder and name it CourseController. Write the following code in it:

[Route("api/[controller]")]
public class CoursesController : Controller
{
  CourseRepository _repository;

  public CoursesController(CourseRepository repository)
  {
    this._repository = repository;
  }

  [HttpGet]
  public IEnumerable GetAllCourses()   {     return _repository.GetCourses((ClaimsIdentity)User.Identity);   } }  

Run the application, open a browser and change the URL to http://localhost: /api/courses. You will see the list of courses in JSON format in the browser.

REST API for CourseTrackers

The course trackers API will provide APIs to list, add or update tracker based on the currently logged in user. So all APIs in this controller would be restricted and hence we need to decorate them with the Authorize attribute.

Let’s create a repository class to perform the required operations on CourseTrackers table. The repository class would contain the following three methods:

  • A method to get details of trackers by user. It accepts user ID of the logged in user and returns a list of Data Transfer Object containing tracker ID, course title, author name and if the tracker is completed
  • A method to add a new track
  • A method to mark an existing track completed

Let’s create the DTO class. As described, it will have four properties that will be shown on the screen. Add a new class to the Models folder and name it CourseTrackerDTO. Following is the code in this class:

public class CourseTrackerDTO
{
  public int CourseTrackerId { get; set; }
  public string CourseTitle { get; set; }
  public string Author { get; set; }
  public bool IsCompleted { get; set; }
}

Add another class to the Models folder and name it CourseTrackerRepository. Replace code in this file with the following:

public class CourseTrackerRepository
{
    ApplicationDbContext _context;

    public CourseTrackerRepository(ApplicationDbContext context)
    {
        this._context = context;
    }

    public IEnumerable GetCourseTrackers(string userId)     {         var userTrackers = _context.CourseTrackers.Where(ct => ct.ApplicationUserId == userId);         var trackerDtoList = new List();          foreach (var tracker in userTrackers)         {             var course = _context.Courses.FirstOrDefault(c => c.CourseID == tracker.CourseID);              trackerDtoList.Add(new CourseTrackerDTO()             {                 CourseTrackerId = tracker.CourseTrackerID,                 CourseTitle = course.Title,                 Author = course.Author,                 IsCompleted = tracker.IsCompleted             });         }          return trackerDtoList;     }      public CourseTracker UpdateCourseTracker(CourseTracker updatedTracker)     {         var tracker = _context.CourseTrackers.FirstOrDefault(ct => ct.CourseTrackerID == updatedTracker.CourseTrackerID);          if (tracker != null)         {             tracker.IsCompleted = updatedTracker.IsCompleted;              _context.Entry(tracker).SetState(Microsoft.Data.Entity.EntityState.Modified);              _context.SaveChanges();             return tracker;         }          return null;     }      public CourseTracker AddTracker(CourseTracker tracker)     {         tracker.IsCompleted = false;          if (_context.CourseTrackers.FirstOrDefault(ct => ct.CourseID == tracker.CourseID && tracker.ApplicationUserId == tracker.ApplicationUserId) == null)         {             var addedTracker = _context.Add(tracker);             _context.SaveChanges();             return tracker;         }          return null;     } }  

The controller has to simply call the methods of the repository class. Before we do that, we need to register the repository class to the IoC container of the environment. Add the following statement to the ConfigureServices method of the Startup class:

services.AddTransient(typeof(CourseTrackerRepository), typeof(CourseTrackerRepository));

Now we can inject this repository inside the controller and use it. Add a new MVC Controller to the Controllers folder and name it CourseTrackersController. Following is the controller with the Get method. Add and update methods are straight forward, you can refer to them from the sample code.

[Route("api/[controller]")]
public class CourseTrackersController : Controller
{
  CourseTrackerRepository _repository;

  public CourseTrackersController(CourseTrackerRepository repository)
  {
    this._repository = repository;
  }

  [HttpGet]
  [Authorize]
  public IEnumerable GetCoursesOfUser()   {     var userId = ((ClaimsIdentity)User.Identity).Claims.FirstOrDefault().Value;     var trackers = _repository.GetCourseTrackers(userId);     return trackers;   } }  

 

Building Views and Binding Data to them using AngularJS

Let’s start modifying the index.cshtml page present in Home folder under Views and make it behave as a Single Page Application. Delete all of the existing mark-up inside the Index.cshtml file. Before we start adding AngularJS content to this file, let’s add the required scripts to the _Layout.cshtml page. We need to add the scripts files of Angular.js, Angular routes and the concatenated script file generated from the Grunt process. We also need to add a section that will be used in the index page to set a global variable with user’s ID. Following are the script tags:



@RenderSection("userScript", required: false)


At the end of the index.cshtml page, let’s add the userScript section to store the user ID in a global variable. We will use this variable later in the Angular code. Following is the script:

@section userScript{
    
}

We need to set value of the UserName to ViewBag object inside the Home controller. To do so, modify the Index action method as follows:

public IActionResult Index()
{
    if (User.Identity.IsAuthenticated)
    {
        ViewBag.UserName = User.Identity.GetUserName();
    }
    return View();
}

Defining Views and Angular Routes

The application will have just two pages: One to list all of the courses and the second view to list all tracked courses. To define a module and routes, add a new JavaScript file to the Scripts folder under App folder and name it app.js. Add following code to it:

(function(){
    angular.module('virtualTrainingApp', ['ngRoute', 'virtualTrainingTemplates'])
        .config(['$routeProvider', function($routeProvider){
            //TODO: Configure routes
            $routeProvider.when('/courseList', {
                templateUrl: 'App/Templates/courseList.html',
                controller: 'courseListCtrl',
                controllerAs: 'vm'
            })
            .when('/courseTracks', {
                templateUrl: 'App/Templates/courseTracks.html',
                controller: 'courseTrackCtrl',
                controllerAs: 'vm'
            })
            .when('/courseTracks2', {
                templateUrl: 'App/Templates/courseTracks.html',
                controller: 'courseTrackCtrl',
                controllerAs: 'vm'
            })
            .otherwise({redirectTo: '/courseList'});
        }]);
}());

I am using “controller as” syntax in the views. This feature is added in Angular 1.2 and it is the recommended way of hooking controllers with views these days. You can read my blog post on this topic to get an overview of it.

The HTML template inside the file index.html has to load this module, have a navigation bar and it has to define a placeholder, where the views will be loaded. The navigation bar has to be shown to only logged-in users, as the anonymous users can’t set the courses in watching state. Following is the mark-up inside the view:



Add a new HTML file to the templates folder and name it courseList.html. This view performs the following functionality:

  • Shows the list of courses available
  • Allows a logged-in user to start watching a course
  • If a logged-in user has already started watching a course or has finished watching, the corresponding status has to be shown
  • The option of starting to watch a course or, the status of course completion shouldn’t be shown to an anonymous user

Following is the mark-up on courseList.html:

{{course.title}}
by {{course.author}}

{{course.duration}} hr
Watching... Watched

The second and last view of the application is, the courseTrack view. Add a new HTML file to the Templates folder and name it courseTrackers.html. This view performs the following functionality:

  • Lists the courses in progress and completed courses in a table
  • The user can mark the course as completed using this screen

Following is the mark-up of the page:

Status of Courses Tracked

Course Title Author Completed?
{{track.courseTitle}} {{track.author}} Completed

Building Angular Services

The application needs two services: One to store logged-in status of the user and the other is to call the HTTP APIs.

The service to store User information looks for the global variable that we added to the index.cshtml page and then stores the user name in a variable and also stores a flag indicating if a user is currently logged-in. It exposes two functions that can be used to get this information from the service.

Add a new JavaScript file to the Scripts folder and name it userInfo.js. Following is the code to be placed in the service:

(function (module) {
    module.service('userInfoSvc', ['$window', function ($window) {
        var isUserAuthenticated = false, userName;
 
        if ($window.userName) {
            isUserAuthenticated = true;
            userName = $window.userName;
        }
 
        this.getUserName = function () {
            return userName;
        };
 
        this.isAuthenticated = function () {
            return isUserAuthenticated;
        };
    }]);
}(angular.module('virtualTrainingApp')));

The second service abstracts the HTTP calls. The controllers would be using this service to get data from the Web API services that we created earlier. Logic of the service is straight-forward, as there are no decisions to be taken in it.

Add a new JavaScript file to the Scripts folder and name it dataSvc.js. Add the following code to it:

(function (module) {
    module.factory('coursesDataSvc', ['$http', function ($http) {
        function getAllCourses() {
            return $http.get('/api/courses').then(function (result) {
                return result.data;
            }, function (error) {
                return error;
            });
        }
 
        function getCourseTracks() {
            return $http.get('/api/courseTrackers').then(function (result) {
                return result.data;
            }, function (error) {
                return error;
            });
        }
 
        function addCourseToTrack(newCourse) {
            return $http.post('/api/courseTrackers', {
                courseId: newCourse.courseId
            }).then(function (result) {
                return result;
            }, function (error) {
                return error;
            });
        };
 
        function modifyCourseTrack(courseTrackDetails) {
            return $http.put('/api/courseTrackers/' + courseTrackDetails.courseTrackerId, {
                courseTrackerId: courseTrackDetails.courseTrackerId,
                isCompleted: true
            }).then(function (result) {
                return result;
            }, function (error) {
                return error;
            });
        };
 
        return {
            getAllCourses: getAllCourses,
            getCourseTracks: getCourseTracks,
            addCourseToTrack: addCourseToTrack,
            modifyCourseTrack: modifyCourseTrack
        };
    }]);
}(angular.module('virtualTrainingApp')));

Creating Angular Controllers

The application needs three controllers, one for each of the views and the other one for the navigation menu.

As already stated, the navigation menu will be visible only to the logged-in users and it will have two menu options. The controller does the following to control behavior of the navigation menu:

  • Gets user information from the userInfoSvc service and sets a Boolean flag to show or, hide the navigation menu
  • Stores navigation links and text to be displayed on the links in a collection

This controller contains the following code:

(function (module) {
    module.controller('navigationCtrl', ['userInfoSvc', function (userInfoSvc) {
        var nav = this;
        nav.navigationList = [{ url: '#/courseList', text: 'Course List' }, { url: '#/courseTracks', text: 'Course Track' }];
        nav.isAuthenticated = userInfoSvc.isAuthenticated();
    }]);
}(angular.module('virtualTrainingApp')));

As you see, I didn’t inject $scope to the above controller and the data to be exposed to the view has been added to the instance of the controller. Angular internally adds the instance of the controller to the $scope that gets created behind the scenes and hence this object is available to the view.

The controller for the courseList view does the following:

  • Fetches the list of courses and stores them in the controller’s instance
  • Checks if the user is authenticated and if yes, it provides a method to add a course to the tracker of the user

This controller contains the following code:

(function (module) {
    module.controller('courseListCtrl', ['userInfoSvc', 'coursesDataSvc', function (userInfoSvc, coursesDataSvc) {
        var vm = this;
 
        function init() {
            coursesDataSvc.getAllCourses().then(function (result) {
                vm.courses = result.data;
            }, function (error) {
                console.log(error);
            });
        }
 
        vm.isAuthenticated = userInfoSvc.isAuthenticated();
 
        if (vm.isAuthenticated) {
            vm.addTrack = function (courseId) {
                coursesDataSvc.addCourseToTrack({
                    courseId: courseId
                }).then(function (result) {
                    init();
                }, function (error) {
                    console.log(error);
                });
            };
        }
 
        init();
    }]);
}(angular.module('virtualTrainingApp')));

The third and last controller of the application is, courseTrackCtrl. This controller does the following:

  • Gets the list of courses that the user is currently watching or, already watched
  • Provides a method using which the user can mark the course as completed

Here’s the code for this controller:

(function (module) {
    module.controller('courseTrackCtrl', ['userInfoSvc', 'coursesDataSvc', function (userInfoSvc, coursesDataSvc) {
        var vm = this;
 
        function init() {
            coursesDataSvc.getCourseTracks().then(function (result) {
                vm.courses = result.data;
            });
        }
 
        vm.completeCourse = function (courseTrackerId) {
            coursesDataSvc.modifyCourseTrack({ courseTrackerId: courseTrackerId })
                .then(function (result) {
                    init();
                }, function (error) {
                    console.log(error);
                });
        };
 
        init();
    }]);
}(angular.module('virtualTrainingApp')));

Now that we have the entire application ready, run the application. You should be able to see a screen similar to the one shown below:

angular-anonymous-user

Now register and create a new user. If you have already created a user, login using the credentials and you will see a slightly different look on the screen. Following is a screenshot of the page after logging in:

loggedin-user

Conclusion

I think, by now you got a good idea of the way ASP.NET 5 is designed and how it welcomes usage of some of the coolest modern tools to make web development enjoyable on the platform. The new architecture of the framework makes it easier to build applications on it and it also seems more open for developing rich applications using client side MVC frameworks like AngularJS. ASP.NET 5 is still under development and ASP.NET team is putting together a lot of work to make the framework even better by the time it is ready for market. Let’s wait and see what more Microsoft has to pour into our buckets.

Download the entire source code from GitHub at bit.ly/dncm18-aspnet5-angular

This article has been editorially reviewed by Suprotim Agarwal.

Absolutely Awesome Book on C# and .NET

C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.

We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).

Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.

Click here to Explore the Table of Contents or Download Sample Chapters!

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Author
Rabi Kiran (a.k.a. Ravi Kiran) is a developer working on Microsoft Technologies at Hyderabad. These days, he is spending his time on JavaScript frameworks like AngularJS, latest updates to JavaScript in ES6 and ES7, Web Components, Node.js and also on several Microsoft technologies including ASP.NET 5, SignalR and C#. He is an active blogger, an author at SitePoint and at DotNetCurry. He is rewarded with Microsoft MVP (Visual Studio and Dev Tools) and DZone MVB awards for his contribution to the community


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!