在 VS 2015 中使用 ASP.NET Core 模板包创建 Angular2 应用程序
在 VS 2015 中使用 ASP.NET Core 模板包创建 Angular2 应用程序
目录
引言
让我们在 VS 2015 中使用 ASP.NET Core 模板包创建一个 Web 应用程序,此模板包含所有用于 Angular2 和 ASP.NET Core 的配置。
背景
在 Web 应用程序开发中,我们需要将 RESTful API 与 UI 集成,现在 Angular2 已发布最终版本,因此我们将开发一个集成 ASP.NET Core 与 Angular2 的 Web 应用程序,此模板包含我们开发 Web 应用程序所需的所有配置。
技能先决条件
- C#
- ORM(对象关系映射)
- RESTful 服务
- TypeScript
- Angular 2
软件先决条件
Using the Code
为您的 Visual Studio 下载并安装模板(检查软件先决条件),感谢 Oscar Agreda 分享模板的下载链接。
步骤 01 - 在 Visual Studio 中创建项目
我们将模板添加到 Visual Studio 后,打开 Visual Studio 并创建一个新项目
将项目名称设置为 OrderViewer
并单击“确定”。
创建项目后,我们可以运行项目并得到以下输出
步骤 02 - 添加后端代码
如果我们不了解如何配置 Web API 以使用 EF Core 访问 SQL Server 实例,请查看相关链接部分。
此时,我们将使用 Sales 模式:订单头和详细信息来显示订单信息。
我们需要为项目创建以下目录
- Extensions:扩展方法的占位符
- Models:与数据库访问、建模和配置相关的对象的占位符
- Responses:表示 Http 响应的对象的占位符
此时的代码将是
SalesController.cs
类代码
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
接口代码
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
类代码
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
类代码
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;
}
}
}
我们可以返回一个匿名类型,但我们希望很快添加单元测试,最好有一个类型化的结构,因为这样更容易知道我们与客户端共享哪些信息
一旦后端构建无错误,我们可以在浏览器中测试以下 URL
Url | 描述 |
---|---|
api/Sales/Order | 检索所有订单,使用默认页面大小和页码 |
api/Sales/Order?salesOrderNumber=so7 | 检索所有与销售订单号“so72”匹配的订单 |
api/Sales/Order?customerName=her | 检索所有与客户名称“hey”匹配的订单 |
api/Sales/Order?salesOrderNumber=so72&customerName=ha | 检索所有与销售订单号“so72”和客户名称“her”匹配的订单 |
api/Sales/Order/75123 | 按 ID 75123 检索一个订单 |
步骤 03 - 为 API 添加帮助页面
有了 API 后,我们现在开始为 API 添加一个帮助页面,首先我们需要为 API 项目添加以下包
包名称 | 版本 |
---|---|
Swashbuckle | 6.0.0-beta902 |
现在保存更改并构建项目,然后我们继续在 Starup
类中应用更改以在项目中启用 Swagger
。
Startup
类代码
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();
}
}
}
保存更改并运行项目,现在请确保控制器中的方法具有 XML 注释,并且 API 项目已启用 XML 文档,最后一步我们可以测试以下 URL
Url | 描述 |
---|---|
https://:[random_port]/swagger/v1/swagger.json | 获取包含 API 描述的 json 文件 |
https://:[random_port]/swagger/ui | 显示带有 API 描述的图形界面 |
Swagger UI
生产操作
销售操作
步骤 04 - 添加前端代码
接下来,将以下代码添加到 app 目录中,正如我们所看到的,所有前端代码都是 TypeScript,现在我们可以讨论什么是 TypeScript 以及为什么我们应该使用它。
- 什么是 TypeScript? 根据 C# 开发总监 Anders H. 的说法,TypeScript 是 JavaScript 的超集,允许我们创建类、接口、类型化对象和其他类似 Java/C# 的函数
- 我可以使用 JavaScript 来开发 Angular2 吗? 是的,事实上我们可以使用纯 JavaScript、Google Dart 或 TypeScript,但默认情况下 Google 建议使用 TypeScript
- 为什么 Google 使用 Microsoft 的技术而不是最大限度地利用 Google dart? 我认为两家公司有协议,但到目前为止我对此没有更多细节,只是猜测
- 这种技术采纳对开源开发者来说是 Google 的背叛吗? 我不这么认为,所有公司都对引领开发行业感兴趣,将这种变化视为背叛是错误的观念,最好将新技术视为一个技术目标:)
转到“解决方案资源管理器”中的“app”目录,删除所有与计数器和获取数据相关的内容,然后创建以下文件
- 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
关于 TypeScript
- 我们可以使用 import 关键字在 typescript 中导入任何引用
- 如果我们要访问类或接口的所有成员,我们需要添加 export 关键字
- typescript 中的声明是:名称: 类型 (例如 firstName: string)
- typescript 中的命名约定更像 javascript 和 java
order-list.component.html
文件代码
<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
文件代码
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
文件代码
<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
文件代码
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
文件代码
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);
}
}
请注意这些方面
- 我们已经创建了一个服务来消费我们的 Web API,而不是将 Http 注入到组件中,这是因为拥有一个注入的服务并在不同的组件中使用它更具重用性,而不是复制/粘贴逻辑来消费 Web API。
app.module.ts
是我们应用程序的核心,在这个文件中我们需要导入所有组件并定义路由表。- 在 app.module.ts 文件中,providers 数组决定了整个应用程序的所有注入服务。
- 在 TypeScript 中,我们不要忘记使用 export 关键字来暴露将被其他对象使用的类或接口。
- 为了编写干净的代码,我们有针对特定对象的特定目录:模型、服务、组件等。
- TypeScript + Angular2 文件的命名约定是小写字母和连字符,加上根据对象类型(组件、服务等)的后缀。
- 我们可以在 Angular 中使用过滤器格式化日期和数字(例如 {{ value | date }})
保存所有更改并构建解决方案,如果构建没有错误,我们可以运行项目并在浏览器中看到以下输出
现在,请单击一个订单的详细信息以查看订单详情
目前,客户端就足够了,因为我们需要了解 TypeScript & Angular2 的基本方面,实际上并不基本 :) ... 创建一个服务并将其注入组件是一种高级开发风格,需要大量的概念和技能才能获得这种编程风格的优势。我很快会为此应用程序添加缺失的功能,请随时添加您的建议以改进此代码。
步骤 05 - 为后端添加单元测试
此时我们将为后端代码添加单元测试,以便自动化代码测试。
打开命令行,导航到解决方案目录并执行这些命令
- mkdir OrderViewer.Tests
- cd OrderViewer.Tests
- dotnet new -t xunittest
- dotnet restore
现在我们应该从 Visual Studio 将我们的测试项目添加到解决方案中,添加后,我们将默认生成的类重命名为 SalesControllerTests.cs 并更改代码为以下内容
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);
}
}
}
单元测试列表
名称 | 描述 |
---|---|
TestGetOrdersAsync | 检索所有订单,使用默认页面大小和页码 |
TestGetOrdersSearchingBySalesOrderNumberAsync | 检索所有与销售订单号“so72”匹配的订单 |
TestGetOrdersSearchingByCustomerNameAsync | 检索所有与客户名称“hey”匹配的订单 |
TestGetOrdersSearchingBySalesOrderNumberAndCustomerNameAsync | 检索所有与销售订单号“so72”和客户名称“her”匹配的订单 |
TestGetOrderAsync | 按 ID 75123 检索一个订单 |
TestGetOrderNotFoundAsync | 按不存在的 ID 检索一个订单 |
现在我们可以保存所有更改并重新构建我们的解决方案,或者我们可以使用此命令从命令行运行单元测试:dotnet test
这些单元测试目前是关于数据读取的,所以我们可以多次运行而不用担心。
重构您的后端代码
正如我们目前所看到的,OrderViewer 项目中有很多对象,作为企业应用程序开发的一部分,不建议将所有对象都放在 UI 项目中,我们将按照以下步骤拆分我们的 UI 项目
- 右键单击解决方案名称
- 添加 > 新项目 > .NET Core
- 将项目名称设置为 OrderViewer.Core
- 好的
现在我们为新项目添加实体框架核心包。不
这是 OrderViewer.Core 项目的结构
- DataLayer
- DataLayer.Contracts
- DataLayer.DataContracts
- DataLayer.Mapping
- EntityLayer
使用下图将所有类重构为单独的文件
将此任务作为您的挑战,一旦您重构了所有代码,将 OrderViewer.Core 项目的引用添加到 OrderViewer 项目,保存所有更改并构建您的解决方案,您将在单元测试项目上遇到错误,因此在单元测试项目中添加命名空间和引用,现在保存所有更改并构建您的解决方案。
额外挑战:为这个应用程序添加一个产品类别列表,可以参考订单列表,无论如何我会在几天内添加,但你可以挑战你的技能:)
如果一切正常,我们可以无错误地运行我们的应用程序。
代码改进
- 为订单列表组件添加分页
- 为订单列表组件添加日期选择器
- 向 UI 添加 Toast 通知
- 用 Material Design 替换 Bootstrap
- 添加集成测试
- 根据您的观点进行其他改进,请在评论中告诉我:)
关注点
- 如果我们详细审查 TypeScript 代码,我们可以看到 C# 代码和 TypeScript 代码之间的一些相似之处,这是因为 TypeScript 是一种类型语言,我们需要复制客户端相同的结构来设置后端结果。
- Angular 1.x 是关于控制器的,Angular 2 是关于 Web Components 的
相关链接
历史
- 2016年11月13日:初始版本
- 2016年11月23日:订单详细信息版本
- 2017年1月24日:添加 API 帮助页面
- 2017年8月27日:添加目录
- 2017年11月17日:代码更新