65.9K
CodeProject 正在变化。 阅读更多。
Home

在 VS 2015 中使用 ASP.NET Core 模板包创建 Angular2 应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (38投票s)

2016年11月13日

CPOL

9分钟阅读

viewsIcon

112071

在 VS 2015 中使用 ASP.NET Core 模板包创建 Angular2 应用程序

目录

  1. 引言
  2. 背景
  3. 技能先决条件
  4. 软件先决条件
  5. Using the Code
  6. 代码改进
  7. 关注点
  8. 相关链接

引言

让我们在 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

软件先决条件

  • 带有 Update 3 的 Visual Studio 2015
  • ASP.NET Core 模板包 下载链接
  • AdventureWorks 数据库 下载
  • Node JS

Using the Code

为您的 Visual Studio 下载并安装模板(检查软件先决条件),感谢 Oscar Agreda 分享模板的下载链接。

步骤 01 - 在 Visual Studio 中创建项目

我们将模板添加到 Visual Studio 后,打开 Visual Studio 并创建一个新项目

New project

将项目名称设置为 OrderViewer 并单击“确定”。

创建项目后,我们可以运行项目并得到以下输出

First Project run

步骤 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

Web API 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

Swagger UI

生产操作

Production Operations

销售操作

Sales Operations

步骤 04 - 添加前端代码

接下来,将以下代码添加到 app 目录中,正如我们所看到的,所有前端代码都是 TypeScript,现在我们可以讨论什么是 TypeScript 以及为什么我们应该使用它。

  1. 什么是 TypeScript? 根据 C# 开发总监 Anders H. 的说法,TypeScript 是 JavaScript 的超集,允许我们创建类、接口、类型化对象和其他类似 Java/C# 的函数
  2. 我可以使用 JavaScript 来开发 Angular2 吗? 是的,事实上我们可以使用纯 JavaScript、Google Dart 或 TypeScript,但默认情况下 Google 建议使用 TypeScript
  3. 为什么 Google 使用 Microsoft 的技术而不是最大限度地利用 Google dart? 我认为两家公司有协议,但到目前为止我对此没有更多细节,只是猜测
  4. 这种技术采纳对开源开发者来说是 Google 的背叛吗? 我不这么认为,所有公司都对引领开发行业感兴趣,将这种变化视为背叛是错误的观念,最好将新技术视为一个技术目标:)

转到“解决方案资源管理器”中的“app”目录,删除所有与计数器和获取数据相关的内容,然后创建以下文件

  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

关于 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);
    }
}

请注意这些方面

  1. 我们已经创建了一个服务来消费我们的 Web API,而不是将 Http 注入到组件中,这是因为拥有一个注入的服务并在不同的组件中使用它更具重用性,而不是复制/粘贴逻辑来消费 Web API。
  2. app.module.ts 是我们应用程序的核心,在这个文件中我们需要导入所有组件并定义路由表。
  3. 在 app.module.ts 文件中,providers 数组决定了整个应用程序的所有注入服务。
  4. 在 TypeScript 中,我们不要忘记使用 export 关键字来暴露将被其他对象使用的类或接口。
  5. 为了编写干净的代码,我们有针对特定对象的特定目录:模型、服务、组件等。
  6. TypeScript + Angular2 文件的命名约定是小写字母和连字符,加上根据对象类型(组件、服务等)的后缀。
  7. 我们可以在 Angular 中使用过滤器格式化日期和数字(例如 {{ value | date }}

保存所有更改并构建解决方案,如果构建没有错误,我们可以运行项目并在浏览器中看到以下输出

Success run

现在,请单击一个订单的详细信息以查看订单详情

Order details view

目前,客户端就足够了,因为我们需要了解 TypeScript & Angular2 的基本方面,实际上并不基本 :) ... 创建一个服务并将其注入组件是一种高级开发风格,需要大量的概念和技能才能获得这种编程风格的优势。我很快会为此应用程序添加缺失的功能,请随时添加您的建议以改进此代码。

步骤 05 - 为后端添加单元测试

此时我们将为后端代码添加单元测试,以便自动化代码测试。

打开命令行,导航到解决方案目录并执行这些命令

  1. mkdir OrderViewer.Tests
  2. cd OrderViewer.Tests
  3. dotnet new -t xunittest
  4. 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

Running test from command line

这些单元测试目前是关于数据读取的,所以我们可以多次运行而不用担心。

重构您的后端代码

正如我们目前所看到的,OrderViewer 项目中有很多对象,作为企业应用程序开发的一部分,不建议将所有对象都放在 UI 项目中,我们将按照以下步骤拆分我们的 UI 项目

  1. 右键单击解决方案名称
  2. 添加 > 新项目 > .NET Core
  3. 将项目名称设置为 OrderViewer.Core
  4. 好的

现在我们为新项目添加实体框架核心包。不

这是 OrderViewer.Core 项目的结构

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

使用下图将所有类重构为单独的文件

Project refactoring structure

将此任务作为您的挑战,一旦您重构了所有代码,将 OrderViewer.Core 项目的引用添加到 OrderViewer 项目,保存所有更改并构建您的解决方案,您将在单元测试项目上遇到错误,因此在单元测试项目中添加命名空间和引用,现在保存所有更改并构建您的解决方案。

额外挑战:为这个应用程序添加一个产品类别列表,可以参考订单列表,无论如何我会在几天内添加,但你可以挑战你的技能:)

如果一切正常,我们可以无错误地运行我们的应用程序。

代码改进

  1. 为订单列表组件添加分页
  2. 为订单列表组件添加日期选择器
  3. 向 UI 添加 Toast 通知
  4. 用 Material Design 替换 Bootstrap
  5. 添加集成测试
  6. 根据您的观点进行其他改进,请在评论中告诉我:)

关注点

  • 如果我们详细审查 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日:代码更新
© . All rights reserved.