- Introduction
- Background
- Skills Prerequisites
- Software Prerequisites
- Using the Code
- Code improvements
- Points of Interest
- Related Links
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
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.
- C#
- ORM (Object Relational Mapping)
- RESTful services
- TypeScript
- Angular 2
- Visual Studio 2015 with Update 3
- ASP.NET Core Template Pack Download link
- AdventureWorks database download
- Node JS
Download and install the template for your Visual Studio (Check software prerequisites), thanks for Oscar Agreda for share the template's download link.
Once we have the template added to our Visual Studio open Visual Studio and create a 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:
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);
}
[HttpGet("Order")]
public async Task<IActionResult> GetOrdersAsync(Int32? pageSize = 10, Int32? pageNumber = 1, String salesOrderNumber = "", String customerName = "")
{
var response = new ListResponse<OrderSummaryViewModel>();
try
{
var query = SalesRepository.GetOrders(salesOrderNumber, customerName);
response.PageSize = (int)pageSize;
response.PageNumber = (int)pageNumber;
response.ItemsCount = await query.CountAsync();
var list = await query.Paging((int)pageSize, (int)pageNumber).ToListAsync();
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();
}
[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)
{
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 |
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; }
public void ConfigureServices(IServiceCollection 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" }
});
var basePath = PlatformServices.Default.Application.ApplicationBasePath;
var xmlPath = Path.Combine(basePath, "OrderViewer.xml");
options.IncludeXmlComments(xmlPath);
});
}
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:
Production Operations:
Sales Operations:
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.
- 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#
- 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
- 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
- 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:
- app/components/sales/order-list.component.html
- app/components/sales/order-list.component.ts
- app/components/sales/order-detail.component.html
- app/components/sales/order-detail.component.ts
- app/responses/list.response.ts
- app/responses/single.response.ts
- app/models/order.detail.ts
- app/models/order.summary.ts
- app/models/order.ts
- app/services/sales.service.ts
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:
- 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.
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. - In app.module.ts file, the providers array determinates all injected services for whole application.
- In TypeScript we don't forget export keyword to expose class or interface that would be use by other objects.
- To write clean code, we have specific directories to specific objects: models, services, components, etc.
- Naming convention for TypeScript + Angular2 files is lower case and dash plus suffix according to object's type: component, service, etc.
- 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:
Now, please clic on details for one order to view order details:
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.
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
- mkdir OrderViewer.Tests
- cd OrderViewer.Tests
- dotnet new -t xunittest
- 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()
{
var repository = RepositoryMocker.GetSalesRepository();
var controller = new SalesController(repository);
var response = await controller.GetOrdersAsync() as ObjectResult;
var value = response.Value as IListResponse<OrderSummaryViewModel>;
repository.Dispose();
Assert.False(value.DidError);
Assert.True(value.Model.Count() > 0);
}
[Fact]
public async Task TestGetOrdersSearchingBySalesOrderNumberAsync()
{
var repository = RepositoryMocker.GetSalesRepository();
var controller = new SalesController(repository);
var salesOrderNumber = "so72";
var response = await controller.GetOrdersAsync(salesOrderNumber: salesOrderNumber) as ObjectResult;
var value = response.Value as IListResponse<OrderSummaryViewModel>;
repository.Dispose();
Assert.False(value.DidError);
}
[Fact]
public async Task TestGetOrdersSearchingByCustomerNameAsync()
{
var repository = RepositoryMocker.GetSalesRepository();
var controller = new SalesController(repository);
var customerName = "her";
var response = await controller.GetOrdersAsync(customerName: customerName) as ObjectResult;
var value = response.Value as IListResponse<OrderSummaryViewModel>;
repository.Dispose();
Assert.False(value.DidError);
}
[Fact]
public async Task TestGetOrdersSearchingBySalesOrderNumberAndCustomerNameAsync()
{
var repository = RepositoryMocker.GetSalesRepository();
var controller = new SalesController(repository);
var salesOrderNumber = "so72";
var customerName = "her";
var response = await controller.GetOrdersAsync(salesOrderNumber: salesOrderNumber, customerName: customerName) as ObjectResult;
var value = response.Value as IListResponse<OrderSummaryViewModel>;
repository.Dispose();
Assert.False(value.DidError);
}
[Fact]
public async Task TestGetOrderAsync()
{
var repository = RepositoryMocker.GetSalesRepository();
var controller = new SalesController(repository);
var id = 75123;
var response = await controller.GetOrderAsync(id) as ObjectResult;
var value = response.Value as ISingleResponse<OrderHeaderViewModel>;
repository.Dispose();
Assert.False(value.DidError);
}
[Fact]
public async Task TestGetOrderNotFoundAsync()
{
var repository = RepositoryMocker.GetSalesRepository();
var controller = new SalesController(repository);
var id = 0;
var response = await controller.GetOrderAsync(id) as ObjectResult;
var value = response.Value as ISingleResponse<OrderHeaderViewModel>;
repository.Dispose();
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
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
- Right click on solution's name
- Add > New Project > .NET Core
- Set project's name to OrderViewer.Core
- 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:
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.
- Add pagination for order list component
- Add date pickers for order list component
- Add toaster notifications to UI
- Replace bootstrap with material design
- Add integration tests
- Another improvement according to your point of view, please let me know in the comments :)
- 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
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