Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating Angular2 Application with ASP.NET Core Template Pack in VS 2015

0.00/5 (No votes)
16 Nov 2017 1  
Creating Angular2 Application with ASP.NET Core Template Pack in VS 2015

Table of Contents

  1. Introduction
  2. Background
  3. Skills Prerequisites
  4. Software Prerequisites
  5. Using the Code
  6. Code improvements
  7. Points of Interest
  8. Related Links

Introduction

Let's create a Web Application with ASP.NET Core Template Pack in VS 2015, this template includes all configurations to work with Angular2 and ASP.NET Core

Background

Related to Web applications development, we need to integrate RESTful APIs with UI, now there is a final release for Angular2, so we'll work developing a web application that integrates ASP.NET Core with Angular2, so this template includes all configurations we need to develop a web application.

Skills Prerequisites

  • C#
  • ORM (Object Relational Mapping)
  • RESTful services
  • TypeScript
  • Angular 2

Software Prerequisites

  • Visual Studio 2015 with Update 3
  • ASP.NET Core Template Pack Download link
  • AdventureWorks database download
  • Node JS

Using the Code

Download and install the template for your Visual Studio (Check software prerequisites), thanks for Oscar Agreda for share the template's download link.

Step 01 - Create Project in Visual Studio

Once we have the template added to our Visual Studio open Visual Studio and create a new project:

New project

Set the project name OrderViewer and click OK.

Once we have project created, we can run the project and we'll get the following output:

First Project run

Step 02 - Add Back-end Code

If we don't have knowledge about how to configure Web API for accessing SQL Server instance with EF Core, please take a look on related links section.

At this moment, we'll work with Sales schema: Order header and details for showing order's information.

We need to create the following directories for project:

  • Extensions: Placeholder for extension methods
  • Models: Placeholder for objects related for database access, modeling and configuration
  • Responses: Placeholder for objects that represent Http responses

The code at this point would be:

SalesController.cs class code:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using OrderViewer.Core.DataLayer.Contracts;
using OrderViewer.Core.DataLayer.Repositories;
using OrderViewer.Responses;
using OrderViewer.ViewModels;

namespace OrderViewer.Controllers
{
    [Route("api/[controller]")]
    public class SalesController : Controller
    {
        private ISalesRepository SalesRepository;

        public SalesController(ISalesRepository repository)
        {
            SalesRepository = repository;
        }

        protected override void Dispose(Boolean disposing)
        {
            SalesRepository?.Dispose();

            base.Dispose(disposing);
        }

        /// <summary>
        /// Retrieves a list of orders that match with criteria
        /// </summary>
        /// <param name="pageSize">Page size</param>
        /// <param name="pageNumber">Page number</param>
        /// <param name="salesOrderNumber">Sales order number</param>
        /// <param name="customerName">Customer name</param>
        /// <returns>A ListModelResponse of OrderSummaryViewModel</returns>
        [HttpGet("Order")]
        public async Task<IActionResult> GetOrdersAsync(Int32? pageSize = 10, Int32? pageNumber = 1, String salesOrderNumber = "", String customerName = "")
        {
            var response = new ListResponse<OrderSummaryViewModel>();

            try
            {
                // Get query
                var query = SalesRepository.GetOrders(salesOrderNumber, customerName);

                // Set information for paging
                response.PageSize = (int)pageSize;
                response.PageNumber = (int)pageNumber;
                response.ItemsCount = await query.CountAsync();

                // Retrieve items
                var list = await query.Paging((int)pageSize, (int)pageNumber).ToListAsync();

                // Set model for response
                response.Model = list.Select(item => item?.ToViewModel());

                response.Message = String.Format("Total of records: {0}", response.Model.Count());
            }
            catch (Exception ex)
            {
                response.SetError(ex);
            }

            return response.ToHttpResponse();
        }

        /// <summary>
        /// Retrieves an existing order by id
        /// </summary>
        /// <param name="id">Order ID</param>
        /// <returns>A SingleModelResponse of OrderHeaderViewModel</returns>
        [HttpGet("Order/{id}")]
        public async Task<IActionResult> GetOrderAsync(Int32 id)
        {
            var response = new SingleResponse<OrderHeaderViewModel>();

            try
            {
                var entity = await SalesRepository.GetOrderAsync(id);

                response.Model = entity?.ToViewModel();
            }
            catch (Exception ex)
            {
                response.SetError(ex);
            }

            return response.ToHttpResponse();
        }
    }
}

ISalesRepository.cs interface code:

using System;
using System.Linq;
using System.Threading.Tasks;
using OrderViewer.Core.DataLayer.DataContracts;
using OrderViewer.Core.EntityLayer;

namespace OrderViewer.Core.DataLayer.Contracts
{
    public interface ISalesRepository : IRepository
    {
        IQueryable<OrderSummary> GetOrders(String salesOrderNumber, String customerName);

        Task<SalesOrderHeader> GetOrderAsync(Int32 orderID);
    }
}

AdventureWorksDbContext.cs class code:

using Microsoft.EntityFrameworkCore;
using OrderViewer.Core.DataLayer.Mapping;

namespace OrderViewer.Core.DataLayer
{
    public class AdventureWorksDbContext : DbContext
    {
        public AdventureWorksDbContext(DbContextOptions<AdventureWorksDbContext> options, IEntityMapper entityMapper)
            : base(options)
        {
            EntityMapper = entityMapper;
        }

        public IEntityMapper EntityMapper { get; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Load all mappings for entities
            EntityMapper.MapEntities(modelBuilder);

            base.OnModelCreating(modelBuilder);
        }
    }
}

SalesRepository.cs class code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using OrderViewer.Core.DataLayer.Contracts;
using OrderViewer.Core.DataLayer.DataContracts;
using OrderViewer.Core.EntityLayer;

namespace OrderViewer.Core.DataLayer.Repositories
{
    public class SalesRepository : Repository, ISalesRepository
    {
        public SalesRepository(AdventureWorksDbContext dbContext)
            : base(dbContext)
        {
        }

        public IQueryable<OrderSummary> GetOrders(String salesOrderNumber, String customerName)
        {
            var query =
                from orderHeader in DbContext.Set<SalesOrderHeader>()
                join customer in DbContext.Set<Customer>()
                    on orderHeader.CustomerID equals customer.CustomerID
                join customerPersonJoin in DbContext.Set<Person>()
                    on customer.PersonID equals customerPersonJoin.BusinessEntityID
                        into customerPersonTemp
                from customerPerson in customerPersonTemp.Where(relation => relation.BusinessEntityID == customer.PersonID).DefaultIfEmpty()
                join customerStoreJoin in DbContext.Set<Store>()
                    on customer.StoreID equals customerStoreJoin.BusinessEntityID
                        into customerStoreTemp
                from customerStore in customerStoreTemp.Where(relation => relation.BusinessEntityID == customer.StoreID).DefaultIfEmpty()
                select new OrderSummary
                {
                    SalesOrderID = orderHeader.SalesOrderID,
                    OrderDate = orderHeader.OrderDate,
                    DueDate = orderHeader.DueDate,
                    ShipDate = orderHeader.ShipDate,
                    SalesOrderNumber = orderHeader.SalesOrderNumber,
                    CustomerID = orderHeader.CustomerID,
                    CustomerName = customerPerson.FirstName + (customerPerson.MiddleName == null ? String.Empty : " " + customerPerson.MiddleName) + " " + customerPerson.LastName,
                    StoreName = customerStore == null ? String.Empty : customerStore.Name,
                    Lines = orderHeader.SalesOrderDetails.Count(),
                    TotalDue = orderHeader.TotalDue
                };

            if (!String.IsNullOrEmpty(salesOrderNumber))
            {
                query = query.Where(item => item.SalesOrderNumber.ToLower().Contains(salesOrderNumber.ToLower()));
            }

            if (!String.IsNullOrEmpty(customerName))
            {
                query = query.Where(item => item.CustomerName.ToLower().Contains(customerName.ToLower()));
            }

            if (String.IsNullOrEmpty(salesOrderNumber) && String.IsNullOrEmpty(customerName))
            {
                query = query.OrderByDescending(item => item.SalesOrderID);
            }

            return query;
        }

        public Task<SalesOrderHeader> GetOrderAsync(Int32 orderID)
        {
            var entity = DbContext
                .Set<SalesOrderHeader>()
                .Include(p => p.CustomerFk.PersonFk)
                .Include(p => p.CustomerFk.StoreFk)
                .Include(p => p.SalesPersonFk)
                .Include(p => p.SalesTerritoryFk)
                .Include(p => p.ShipMethodFk)
                .Include(p => p.BillAddressFk)
                .Include(p => p.ShipAddressFk)
                .Include(p => p.SalesOrderDetails)
                    .ThenInclude(p => p.ProductFk)
                .FirstOrDefaultAsync(item => item.SalesOrderID == orderID);

            return entity;
        }
    }
}

We can return an anonymous type but we want to add unit testing soon, it's better to have a typed structure because in that way it's more easy to know which information we share with our client

One we have the back-end build with no errors, we can test the following urls in our browser:

Web API Urls
Url Description
api/Sales/Order Retrieve all orders with default page size and page number
api/Sales/Order?salesOrderNumber=so7 Retrieve all orders match with sales order number "so72"
api/Sales/Order?customerName=her Retrieve all orders match with customer name "hey"
api/Sales/Order?salesOrderNumber=so72&customerName=ha Retrieve all orders match with sales order number "so72" and customer name "her"
api/Sales/Order/75123 Retrieve one order by id 75123

Step 03 - Add Help Page for API

Once we have the API, now we proceed to add a help page for API, first we need to add the following package for API project:

Package name Version
Swashbuckle 6.0.0-beta902

Now save changes and build the project, now we proceed to apply the changes in Starup class in order to enable Swagger in project.

Startup class code:

using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.SpaServices.Webpack;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.PlatformAbstractions;
using OrderViewer.Core.DataLayer;
using OrderViewer.Core.DataLayer.Contracts;
using OrderViewer.Core.DataLayer.Mapping;
using OrderViewer.Core.DataLayer.Repositories;
using Swashbuckle.Swagger.Model;

namespace OrderViewer
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddMvc();

            services.AddDbContext<AdventureWorksDbContext>(options => options.UseSqlServer(Configuration["AppSettings:ConnectionString"]));

            services.AddScoped<IEntityMapper, AdventureWorksEntityMapper>();
            services.AddScoped<ISalesRepository, SalesRepository>();
            services.AddScoped<IProductionRepository, ProductionRepository>();

            services.AddOptions();

            services.AddSingleton<IConfiguration>(Configuration);

            services.AddSwaggerGen();

            services.ConfigureSwaggerGen(options =>
            {
                options.SingleApiVersion(new Info
                {
                    Version = "v1",
                    Title = "OrderViewer API",
                    Description = "OrderViewer ASP.NET Core Web API",
                    TermsOfService = "None",
                    Contact = new Contact { Name = "C. Herzl", Email = "", Url = "https://twitter.com/hherzl" },
                    License = new License { Name = "Use under LICX", Url = "http://url.com" }
                });

                // Determine base path for the application.
                var basePath = PlatformServices.Default.Application.ApplicationBasePath;

                // Set the comments path for the swagger json and ui.
                var xmlPath = Path.Combine(basePath, "OrderViewer.xml");

                options.IncludeXmlComments(xmlPath);
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
                {
                    HotModuleReplacement = true
                });
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();

            app.UseMvc(routes =>
            {
                routes.MapRoute(name: "default", template: "{controller=Home}/{action=Index}/{id?}");
            });

            app.UseSwagger();

            app.UseSwaggerUi();
        }
    }
}

Save changes and run the project, now make sure that your methods in controllers have xml comments and API project has XML documentation enabled, as last step we can test the following urls:

Url Description
http://localhost:[random_port]/swagger/v1/swagger.json Gets json file with API description
http://localhost:[random_port]/swagger/ui Shows a graphic interface with API description

Swagger UI:

Swagger UI

Production Operations:

Production Operations

Sales Operations:

Sales Operations

Step 04 - Add Front-end Code

Next, add the following code into app directory, as we can see all front-end code is TypeScript, now we can talk about what is TypeScript and why we should to use it.

  1. What is TypeScript? According to Anders H. director of C# development, TypeScript it's a super set of JavaScript, allow to us create classes, interfaces, typed objects and others functions like Java/C#
  2. Can I use JavaScript for Angular2? Yes, in fact we can use pure JavaScript, Google Dart or TypeScript but by default Google recommends to use TypeScript
  3. Why Google uses Microsoft's technology instead of maximize Google dart? I think both companies have an agreement but until now I don't have more details about this only speculation
  4. This technology adoption supposes a treason from Google for open source developers? I don't think so, all companies have the interest to lead the development industry, thinking about this change as treason it's a wrong idea, it's better embrace the new technology as a technical goal :)

Go to app directory in Solution Explorer and remove all related to counter and fetch data and proceed to create the following files:

  1. app/components/sales/order-list.component.html
  2. app/components/sales/order-list.component.ts
  3. app/components/sales/order-detail.component.html
  4. app/components/sales/order-detail.component.ts
  5. app/responses/list.response.ts
  6. app/responses/single.response.ts
  7. app/models/order.detail.ts
  8. app/models/order.summary.ts
  9. app/models/order.ts
  10. app/services/sales.service.ts

Front-end code

About TypeScript:

  • We can import any reference in typescript with import keyword
  • If we want to access all members from class or interface we need to add export keyword
  • Declaration in typescript is: name: type (e.g. firstName: string)
  • Naming convention in typescript it's more javascript and java

order-list.component.html file code:

<h1>Orders</h1>

<fieldset>
    <legend>Search</legend>

    <div class="form-inline">
        <div class="form-group">
            <input type="text" class="form-control" placeholder="Sales Order Number" [(ngModel)]="salesOrderNumber" />
        </div>

        <div class="form-group">
            <input type="text" class="form-control" placeholder="Customer Name" [(ngModel)]="customerName" />
        </div>

        <div class="form-group">
            <select id="pageSize" name="pageSize" class="form-control list-box tri-state" [(ngModel)]="result.pageSize">
                <option value="10" selected="selected">10</option>
                <option value="25">25</option>
                <option value="50">50</option>
                <option value="100">100</option>
            </select>
        </div>

        <div class="form-group">
            <button type="button" class="btn btn-primary glyphicon glyphicon-search" (click)="search()"></button>
        </div>
    </div>
</fieldset>

<br />

<div class="alert alert-info" role="alert" *ngIf="result">
    <span class="glyphicon glyphicon-info-sign"></span>
    <strong>{{ result.message }}</strong>
</div>

<table class="table table-hover" *ngIf="result">
    <tr>
        <th>#</th>
        <th>Order Date</th>
        <th>Ship Date</th>
        <th>Due Date</th>
        <th>Sales Order #</th>
        <th>Customer</th>
        <th>Store</th>
        <th>Total Due</th>
        <th>Lines</th>
        <th></th>
    </tr>
    <tr *ngFor="let item of result.model">
        <td>
            {{ item.salesOrderID }}
        </td>
        <td>
            {{ item.orderDate | date: "shortDate" }}
        </td>
        <td>
            {{ item.shipDate | date: "shortDate" }}
        </td>
        <td>
            {{ item.dueDate | date: "shortDate" }}
        </td>
        <td>
            {{ item.salesOrderNumber }}
        </td>
        <td>
            {{ item.customerName }}
        </td>
        <td>
            {{ item.storeName }}
        </td>
        <td style="text-align: right;">
            {{ item.totalDue | currency }}
        </td>
        <td style="text-align: right;">
            {{ item.lines }}
        </td>
        <td>
            <div class="btn-group">
                <button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown">
                    <span class="caret"></span>
                </button>
                <ul class="dropdown-menu">
                    <li><a (click)="details(item)">Details</a></li>
                </ul>
            </div>
        </td>
    </tr>
</table>

<div *ngIf="result">
    Page <strong>{{ result.pageNumber }}</strong> of <strong>{{ result.pageCount }}</strong>
</div>

order-list.component.ts file code:

import { Component, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { IListResponse, ListResponse } from "../../responses/list.response";
import { OrderSummary } from "../../models/order.summary";
import { SalesService } from "../../services/sales.service";

@Component({
    selector: "order-list",
    template: require("./order-list.component.html")
})
export class OrderListComponent implements OnInit {
    public salesOrderNumber: string;
    public customerName: string;
    public result: IListResponse<OrderSummary>;

    constructor(private router: Router, private service: SalesService) {
        this.result = new ListResponse<OrderSummary>();
    }

    ngOnInit(): void {
        this.search();
    }

    search(): void {
        this.service
            .getOrders(this.result.pageNumber, this.result.pageSize, this.salesOrderNumber, this.customerName)
            .subscribe(result => {
                this.result = result.json();
            });
    }

    details(order: OrderSummary): void {
        this.router.navigate(["/order-detail/", order.salesOrderID]);
    }
}

order-detail.component.html file code:

<h2>Order Detail</h2>

<style>
    .dl-horizontal dt {
        clear: left;
        float: left;
        overflow: hidden;
        text-align: right;
        text-overflow: ellipsis;
        width: 170px;
        white-space: nowrap;
  }
</style>

<div *ngIf="result">
    <div>
        <dl class="dl-horizontal">
            <dt>Revision Number</dt>
            <dd>{{ result.model.revisionNumber }}</dd>

            <dt>Order Date</dt>
            <dd>{{ result.model.orderDate | date: "short" }}</dd>

            <dt>Due Date</dt>
            <dd>{{ result.model.dueDate | date: "short" }}</dd>

            <dt>Ship Date</dt>
            <dd>{{ result.model.shipDate | date: "short" }}</dd>

            <dt>Sales Order Number</dt>
            <dd>{{ result.model.salesOrderNumber }}</dd>

            <dt>Purchase Order Number</dt>
            <dd>{{ result.model.purchaseOrderNumber }}</dd>

            <dt>Account Number</dt>
            <dd>{{ result.model.accountNumber }}</dd>

            <dt>Customer Name</dt>
            <dd>{{ result.model.customerName }}</dd>

            <dt>Store Name</dt>
            <dd>{{ result.model.storeName }}</dd>

            <dt>Sales Person Name</dt>
            <dd>{{ result.model.salesPersonName }}</dd>

            <dt>Territory Name</dt>
            <dd>{{ result.model.territoryName }}</dd>

            <dt>Ship Method</dt>
            <dd>{{ result.model.shipMethodName }}</dd>

            <dt>Sub Total</dt>
            <dd>{{ result.model.subTotal | currency }}</dd>

            <dt>Tax Amt</dt>
            <dd>{{ result.model.taxAmt | currency }}</dd>

            <dt>Freight</dt>
            <dd>{{ result.model.freight | currency }}</dd>

            <dt>Total Due</dt>
            <dd>{{ result.model.totalDue | currency }}</dd>

            <dt>Comment</dt>
            <dd>{{ result.model.comment }}</dd>

            <dt>Modified Date</dt>
            <dd>{{ result.model.modifiedDate | date: "short" }}</dd>
        </dl>
    </div>

    <h3>Billing & Shipping</h3>

    <table class="table table-hover">
        <tr>
            <th colspan="2">Bill Address</th>
            <th colspan="2">Ship Address</th>
        </tr>
        <tr>
            <td>Address line 1</td>
            <td>{{ result.model.billAddress.addressLine1 }}</td>
            <td>Address line 1</td>
            <td>{{ result.model.shipAddress.addressLine1 }}</td>
        </tr>
        <tr>
            <td>Address line 2</td>
            <td>{{ result.model.billAddress.addressLine2 }}</td>
            <td>Address line 2</td>
            <td>{{ result.model.shipAddress.addressLine2 }}</td>
        </tr>
        <tr>
            <td>City</td>
            <td>{{ result.model.billAddress.city }}</td>
            <td>City</td>
            <td>{{ result.model.shipAddress.city }}</td>
        </tr>
        <tr>
            <td>Postal code</td>
            <td>{{ result.model.billAddress.postalCode }}</td>
            <td>Postal code</td>
            <td>{{ result.model.shipAddress.postalCode }}</td>
        </tr>
    </table>

    <h3>Details</h3>

    <table class="table table-hover">
        <tr>
            <th>Product name</th>
            <th style="text-align: right;">
                Unit price
            </th>
            <th style="text-align: right;">
                Quantity
            </th>
            <th style="text-align: right;">
                Unit price discount
            </th>
            <th style="text-align: right;">
                Line total
            </th>
        </tr>
        <tr *ngFor="let item of result.model.orderDetails">
            <td>
                {{ item.productName }}
            </td>
            <td style="text-align: right;">
                {{ item.unitPrice | currency }}
            </td>
            <td style="text-align: right;">
                {{ item.orderQty }}
            </td>
            <td style="text-align: right;">
                {{ item.unitPriceDiscount }}
            </td>
            <td style="text-align: right;">
                {{ item.lineTotal | currency }}
            </td>
        </tr>
        <tr>
            <td></td>
            <td></td>
            <td></td>
            <td></td>
            <td style="text-align: right;">
                <strong>
                    {{ result.model.total | currency }}
                </strong>
            </td>
        </tr>
    </table>
</div>

<p>
    <a (click)="backToList()">Back to list</a>
</p>

order-detail.component.ts file code:

import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Params, Router } from "@angular/router";
import { Location } from "@angular/common";
import { ISingleResponse } from "../../responses/single.response";
import { Order } from "../../models/order";
import { SalesService } from "../../services/sales.service";

@Component({
    selector: "order-detail",
    template: require("./order-detail.component.html")
})
export class OrderDetailComponent implements OnInit {
    public result: ISingleResponse<Order>;

    constructor(private route: ActivatedRoute,
        private location: Location,
        private router: Router,
        private service: SalesService) {
    }

    ngOnInit(): void {
        this.loadData();
    }

    loadData(): void {
        this.route.params.forEach((params: Params) => {
            let id = +params["id"];

            this.service.getOrder(id).subscribe(result => {
                this.result = result.json();
            });
        });
    }

    backToList(): void {
        this.router.navigate(["/order"]);
    }
}

sales.service.ts file code:

import { Injectable } from "@angular/core";
import { Http } from "@angular/http";
import { Response } from "@angular/http";
import { Observable } from "rxjs/Observable";

export interface ISalesService {
    getOrders(pageNumber: number,
        pageSize: number,
        salesOrderNumber: string,
        customerName: string): Observable<Response>;

    getOrder(id: number): Observable<Response>;
}

@Injectable()
export class SalesService implements ISalesService {
    constructor(public http: Http) {
    }

    getOrders(pageNumber: number,
        pageSize: number,
        salesOrderNumber: string,
        customerName: string): Observable<Response> {
        var url: string = "/api/Sales/Order?" +
            "pageNumber=" + (pageNumber ? pageNumber : 1) +
            "&pageSize=" + (pageSize ? pageSize : 10) +
            "&salesOrderNumber=" + (salesOrderNumber ? salesOrderNumber : "") +
            "&customerName=" + (customerName ? customerName : "");

        return this.http.get(url);
    }

    getOrder(id: number): Observable<Response> {
        return this.http.get("/api/Sales/Order/" + id);
    }
}

Please take care about these aspects:

  1. We have created a service for consuming our Web API instead of inject Http inside of component, this is because it's more reusable to have an injected service and use in different components instead of copy/paste logic to consume Web API.
  2. app.module.ts is the core of our application and inside of this file we need to import all components and define the route table.
  3. In app.module.ts file, the providers array determinates all injected services for whole application.
  4. In TypeScript we don't forget export keyword to expose class or interface that would be use by other objects.
  5. To write clean code, we have specific directories to specific objects: models, services, components, etc.
  6. Naming convention for TypeScript + Angular2 files is lower case and dash plus suffix according to object's type: component, service, etc.
  7. We can format dates and numbers in angular with filters (e.g. {{ value | date }})

Save all changes and build solution, if the build have no errors, we can run the project and see the following output on our browser:

Success run

Now, please clic on details for one order to view order details:

Order details view

For now, this is enough in client side because we need to understand the basic aspects of TypeScript & Angular2, really aren't not basic :) ... Create a service and inject to component it's an advanced development style and requires a lot of concepts and skills to getting the advantages of this programming style. Soon I'll add the missing features for this application, please feel free to add your suggestions to improve this code.

Step 05 - Add Unit Tests for Back-end

At this point we'll add unit tests for our back-end code in order to automatize testing in the code.

Open a command line and navigate to solution's directory and execute these commands

  1. mkdir OrderViewer.Tests
  2. cd OrderViewer.Tests
  3. dotnet new -t xunittest
  4. dotnet restore

Now we should add our tests project in solution from Visual Studio, once we have added now we proceed to rename the default generated class to SalesControllerTests.cs and add change the code to this:

using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using OrderViewer.Controllers;
using OrderViewer.Responses;
using OrderViewer.ViewModels;
using Xunit;

namespace OrderViewer.Tests
{
    public class SalesControllerTests
    {
        [Fact]
        public async Task TestGetOrdersAsync()
        {
            // Arrange
            var repository = RepositoryMocker.GetSalesRepository();
            var controller = new SalesController(repository);

            // Act
            var response = await controller.GetOrdersAsync() as ObjectResult;
            var value = response.Value as IListResponse<OrderSummaryViewModel>;

            repository.Dispose();

            // Assert
            Assert.False(value.DidError);
            Assert.True(value.Model.Count() > 0);
        }

        [Fact]
        public async Task TestGetOrdersSearchingBySalesOrderNumberAsync()
        {
            // Arrange
            var repository = RepositoryMocker.GetSalesRepository();
            var controller = new SalesController(repository);
            var salesOrderNumber = "so72";

            // Act
            var response = await controller.GetOrdersAsync(salesOrderNumber: salesOrderNumber) as ObjectResult;
            var value = response.Value as IListResponse<OrderSummaryViewModel>;

            repository.Dispose();

            // Assert
            Assert.False(value.DidError);
        }

        [Fact]
        public async Task TestGetOrdersSearchingByCustomerNameAsync()
        {
            // Arrange
            var repository = RepositoryMocker.GetSalesRepository();
            var controller = new SalesController(repository);
            var customerName = "her";

            // Act
            var response = await controller.GetOrdersAsync(customerName: customerName) as ObjectResult;
            var value = response.Value as IListResponse<OrderSummaryViewModel>;

            repository.Dispose();

            // Assert
            Assert.False(value.DidError);
        }

        [Fact]
        public async Task TestGetOrdersSearchingBySalesOrderNumberAndCustomerNameAsync()
        {
            // Arrange
            var repository = RepositoryMocker.GetSalesRepository();
            var controller = new SalesController(repository);
            var salesOrderNumber = "so72";
            var customerName = "her";

            // Act
            var response = await controller.GetOrdersAsync(salesOrderNumber: salesOrderNumber, customerName: customerName) as ObjectResult;
            var value = response.Value as IListResponse<OrderSummaryViewModel>;

            repository.Dispose();

            // Assert
            Assert.False(value.DidError);
        }

        [Fact]
        public async Task TestGetOrderAsync()
        {
            // Arrange
            var repository = RepositoryMocker.GetSalesRepository();
            var controller = new SalesController(repository);
            var id = 75123;

            // Act
            var response = await controller.GetOrderAsync(id) as ObjectResult;
            var value = response.Value as ISingleResponse<OrderHeaderViewModel>;

            repository.Dispose();

            // Assert
            Assert.False(value.DidError);
        }

        [Fact]
        public async Task TestGetOrderNotFoundAsync()
        {
            // Arrange
            var repository = RepositoryMocker.GetSalesRepository();
            var controller = new SalesController(repository);
            var id = 0;

            // Act
            var response = await controller.GetOrderAsync(id) as ObjectResult;
            var value = response.Value as ISingleResponse<OrderHeaderViewModel>;

            repository.Dispose();

            // Assert
            Assert.False(value.DidError);
        }
    }
}

Unit tests list:

Name Description
TestGetOrdersAsync Retrieve all orders with default page size and page number
TestGetOrdersSearchingBySalesOrderNumberAsync Retrieve all orders match with sales order number "so72"
TestGetOrdersSearchingByCustomerNameAsync Retrieve all orders match with customer name "hey"
TestGetOrdersSearchingBySalesOrderNumberAndCustomerNameAsync Retrieve all orders match with sales order number "so72" and customer name "her"
TestGetOrderAsync Retrieve one order by id 75123
TestGetOrderNotFoundAsync Retrieve one order by not existing id

Now we can save all changes and rebuild our solution or we can run the unit tests from command line with this command: dotnet test

Running test from command line

These unit tests are about data reading at this moment, so we can run more than one time with no concern.

Refactor your Back-end code

As we can see at this point, we have many objects into OrderViewer project, as part of enterprice applications development it's not recommended to have all objects into UI project, we going to split our UI project follow these steps

  1. Right click on solution's name
  2. Add > New Project > .NET Core
  3. Set project's name to OrderViewer.Core
  4. OK

Now we add the entity framework core packages for new project.no

This is the structure for OrderViewer.Core project:

  • DataLayer
  • DataLayer.Contracts
  • DataLayer.DataContracts
  • DataLayer.Mapping
  • EntityLayer

Use the following image and refactor all classes to individual files:

Project refactoring structure

Take this task as a challenge for you, one you have refactor all your code, add reference to OrderViewer.Core project to OrderViewer project, save all changes and build your solution, you'll get errors on unit tests project, so add namespaces and reference in unit tests project, now save all changes and build your solution.

Extra challenge: add a product category list for this application, take a references from order list, anyway I'll add in some days but you can challenge your skills :)

If everything it's fine, we can run without errors our application.

Code Improvements

  1. Add pagination for order list component
  2. Add date pickers for order list component
  3. Add toaster notifications to UI
  4. Replace bootstrap with material design
  5. Add integration tests
  6. Another improvement according to your point of view, please let me know in the comments :)

Points of Interest

  • If we get a detail review on TypeScript code, we can see some similarity between C# code and TypeScript code, this is because TypeScript it's a typed language and we need to replicate the same structure from client side to set back-end results.
  • Angular 1.x was about Controllers, Angular 2 it's about Web Components

Related Links

History

  • 13th November, 2016: Initial version
  • 23th November, 2016: Order Detail version
  • 24th January, 2017: Addition of Help Page for API
  • 27th August, 2017: Addition of Table of Contents
  • 17th November, 2017: Code update

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here