用VSCode開發一個基於asp.net core 2.0/sql server linux(docker)/ng5/bs4的專案(2)

solenovex發表於2018-03-01

第一部分: http://www.cnblogs.com/cgzl/p/8478993.html

為Domain Model新增約束

前一部分, 我們已經把資料庫建立出來了. 那麼我們先看看這個資料庫.

可以在專案裡面建立一個database.sql, 並且建立一個資料庫連線的profile(參考上一篇文章), 連線成功後執行下面語句:

SELECT TABLE_NAME FROM tvdb.INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE';

右側結果可以看到建立的table, 其中一個是遷移表, 另外兩個是Domain Model所對應的業務表.

使用下面的sql語句查詢表的欄位定義:

select * from information_schema.columns where table_name = 'TvNetworks';
select * from information_schema.columns where table_name = 'TvShows';

從結果的CHARACTER_MAXIMUM_LENGTH欄位可以看出, 目前name欄位的型別都是nvarchar(max):

這可能不是我們想要的, 所以就需要為Domain Model的相應屬性新增一些約束.

開啟TvNetwork和TvShow, 為name屬性新增約束:

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations;

namespace Tv.Models
{
    public class TvNetwork
    {
        public TvNetwork()
        {
            TvShows = new Collection<TvShow>();
        }
        public int Id { get; set; }
        [Required]
        [StringLength(50)]
        public string Name { get; set; }
        public ICollection<TvShow> TvShows { get; set; }
    }
}
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations;

namespace Tv.Models
{
    public class TvNetwork
    {
        public TvNetwork()
        {
            TvShows = new Collection<TvShow>();
        }
        public int Id { get; set; }
        [Required]
        [StringLength(50)]
        public string Name { get; set; }
        public ICollection<TvShow> TvShows { get; set; }
    }
}

 

EF Core其他的約束屬性請參考文件, 這裡就不介紹了.

這種對Domain Model進行約束的方法使用的是DataAnnotation, 而我個人更喜歡使用FluetApi, 不過在這篇文章裡這個不是重點.

然後新增migrations:

dotnet ef migrations add AddConstraints

 

看一下生成的migration檔案:

沒問題, 可以執行dotnet ef database update了. 執行成功後, 可以看到表的欄位約束已經新增成功了:

為資料庫新增種子資料.

新增種子資料的方法有很多, 可以寫一個方法然後在Startup裡面呼叫. 這裡我使用新增migration的方式:

命令列新增一個空的migration:

dotnet ef migrations add SeedData

 

開啟這個migration檔案, 新增如下程式碼:

using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;

namespace Tv.Migrations
{
    public partial class SeeData : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.Sql("INSERT INTO TvNetworks (Name) VALUES ('Netflix')");
            migrationBuilder.Sql("INSERT INTO TvNetworks (Name) VALUES ('HBO')");
            migrationBuilder.Sql("INSERT INTO TvNetworks (Name) VALUES ('CBS')");
            migrationBuilder.Sql("INSERT INTO TvNetworks (Name) VALUES ('NBC')");

            migrationBuilder.Sql("INSERT INTO TvShows (Name, TvNetworkId) VALUES ('House of Cards', (SELECT Id FROM TvNetworks WHERE Name='Netflix'))");
            migrationBuilder.Sql("INSERT INTO TvShows (Name, TvNetworkId) VALUES ('Altered Carbon', (SELECT Id FROM TvNetworks WHERE Name='Netflix'))");
            migrationBuilder.Sql("INSERT INTO TvShows (Name, TvNetworkId) VALUES ('Marvel''s Daredevil', (SELECT Id FROM TvNetworks WHERE Name='Netflix'))");

            migrationBuilder.Sql("INSERT INTO TvShows (Name, TvNetworkId) VALUES ('Game of Thrones', (SELECT Id FROM TvNetworks WHERE Name='HBO'))");
            migrationBuilder.Sql("INSERT INTO TvShows (Name, TvNetworkId) VALUES ('Silicon Valley', (SELECT Id FROM TvNetworks WHERE Name='HBO'))");
            migrationBuilder.Sql("INSERT INTO TvShows (Name, TvNetworkId) VALUES ('Veep', (SELECT Id FROM TvNetworks WHERE Name='HBO'))");

            migrationBuilder.Sql("INSERT INTO TvShows (Name, TvNetworkId) VALUES ('NCIS', (SELECT Id FROM TvNetworks WHERE Name='CBS'))");
            migrationBuilder.Sql("INSERT INTO TvShows (Name, TvNetworkId) VALUES ('The Big Bang Theory', (SELECT Id FROM TvNetworks WHERE Name='CBS'))");
            migrationBuilder.Sql("INSERT INTO TvShows (Name, TvNetworkId) VALUES ('Criminal Minds', (SELECT Id FROM TvNetworks WHERE Name='CBS'))");

            migrationBuilder.Sql("INSERT INTO TvShows (Name, TvNetworkId) VALUES ('Friends', (SELECT Id FROM TvNetworks WHERE Name='NBC'))");
            migrationBuilder.Sql("INSERT INTO TvShows (Name, TvNetworkId) VALUES ('Chicago Fire', (SELECT Id FROM TvNetworks WHERE Name='NBC'))");
            migrationBuilder.Sql("INSERT INTO TvShows (Name, TvNetworkId) VALUES ('Will & Grace', (SELECT Id FROM TvNetworks WHERE Name='NBC'))");
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.Sql("DELETE FROM TvNetworks WHERE Name IN ('Netflix', 'HBO', 'CBS', 'NBC')");
        }
    }
}

 

然後執行 dotnet ef database update. 成功後可以檢視到資料:

建立Web Api

在Controllers資料夾下建立TvController.cs. 

需要注入TvContext, 這時候聚焦到context變數上使用cmd+. 這個快捷鍵 生成一個field:

隨後, 就會生成一個field:

 完成後到程式碼如下:

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Tv.Database;
using Tv.Models;

namespace Tv.Controllers
{
    public class TvController : Controller
    {
        private readonly TvContext context;

        public TvController(TvContext context)
        {
            this.context = context;
        }

        [HttpGet("/api/tvnetworks")]
        public async Task<IEnumerable<TvNetwork>> GetTvNetworks()
        {
            return await context.TvNetworks.Include(x => x.TvShows).ToListAsync();
        }
    }
}

 

這部分程式碼所涉及到的asp.net core的知識請參考我寫的這個系列文章: http://www.cnblogs.com/cgzl/p/7637250.html

執行專案: dotnet watch run, 這時我們需要使用postman來測試這個api.

以前postman是chrome瀏覽器的一個擴充套件應用, 由於被牆, 可能會安裝不上, 而現在postman是一個獨立的應用了, 應該都能下載安裝了: https://www.getpostman.com/

由於以前講過postman, 所以這裡我就不用postman了. 

Rest Client

我使用vscode擴充套件rest client來測試api. rest client簡介部分可以參考這個文章: http://www.cnblogs.com/cgzl/p/8450409.html

建立一個httptest檔案, 開啟檔案, 使用命令皮膚 輸入查詢這個命令:

然後選擇http:

在檔案中寫下api的uri:

http://localhost:5000/api/tvnetworks

然後你會發現, 該uri的上方有一個send request 按鈕:

點選這個按鈕, 傳送請求.

儘管請求返回結果是200, 但是你也可以發現結果並不正確, 看一下終端命令列:

確實是發生了異常, 因為一個Tvnetwork有個導航屬性是多個TvShow, 而一個TvShow還有一個反向導航屬性是TvNetwork, 所以dbcontext查詢出來在進行json轉化的時候, 會無限迴圈下去, 就引起了self referencing loop.

所以web api 不應該把Domain Model直接暴露出去, 應該使用ViewModel或者叫Dto...

建立ViewModel

建立ViewModels/TvNetworkViewModel.cs 和 TvShowViewModel.cs:

using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace Tv.ViewModels
{
    public class TvNetworkViewModel
    {
        public TvNetworkViewModel()
        {
            TvShows = new Collection<TvShowViewModel>();
        }
        public int Id { get; set; }
        public string Name { get; set; }
        public ICollection<TvShowViewModel> TvShows { get; set; }
    }
}

 

namespace Tv.ViewModels
{
    public class TvShowViewModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int TvNetworkId { get; set; }
    }
}

 

注意TvShowViewModel裡面並沒有反向的TvNetWork屬性, 這也保證了不會發生上面的自身迴圈引用異常.

接下來需要做的就是在Controller裡面把Domain Model的屬性傳遞給ViewModel, 沒人會去手寫這個對映的過程, 所以應該使用AutoMapper等類似的庫

AutoMapper

首先新增AutoMapper, 一共有兩個包:

dotnet add package AutoMapper
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

別忘了還要執行dotnet restore. 

安裝成功後, 在Startup.cs裡面註冊AutoMapper:

此外, AutoMapper還需要知道Domain Model和ViewModel的對應關係和方向.

建立Mapping/MappingProfile.cs:

using AutoMapper;
using Tv.Models;
using Tv.ViewModels;

namespace Tv.Mapping
{
    public class MappingProfile : Profile
    {
        public MappingProfile()
        {
            CreateMap<TvNetwork, TvNetworkViewModel>();
            CreateMap<TvShow, TvShowViewModel>();
        }
    }
}

 

然後在Controller裡面需要注入AutoMapper:

using System.Collections.Generic;
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Tv.Database;
using Tv.Models;
using Tv.ViewModels;

namespace Tv.Controllers
{
    public class TvController : Controller
    {
        private readonly TvContext context;
        private readonly IMapper mapper;

        public TvController(TvContext context, IMapper mapper)
        {
            this.context = context;
            this.mapper = mapper;
        }

        [HttpGet("/api/tvnetworks")]
        public async Task<IEnumerable<TvNetworkViewModel>> GetTvNetworks()
        {
            var models = await context.TvNetworks.Include(x => x.TvShows).ToListAsync();
            var vms = mapper.Map<List<TvNetwork>, List<TvNetworkViewModel>>(models);
            return vms;
        }
    }
}

 

差不多了, 再次測試一下這個api:

沒毛病!!!

建立Angular5專案

按照第一部分的操作安裝好angular cli之後 (https://github.com/angular/angular-cli), 就可以開啟命令列建立angular 客戶端專案了. 使用:

ng new tv-client

 

建立一個名字為tv-client的angular專案. 此時, cli會通過npm自動安裝依賴的包. 

安裝好所有的包之後, 就可以進入該目錄 cd tv-client 並用 vscode開啟該目錄: code+.

這個專案裡面, 我們主要是在src/app裡面寫程式碼, 也會簡單修改一下angular-cli.json檔案.

執行angular專案:

可以使用ng server或者npm start命令執行angular專案:

最好還是使用npm start, 因為ng server以後會需要新增一些引數. 

所以npm start, 看看效果:

開啟瀏覽器 http://localhost:4200,

ok, 專案建立成功了.

由於已經存在種子資料了, 那麼就可以查詢列表了.

建立TvNetwork列表:

首先把當前目錄切換到app下:

根據文件, 使用下面命令建立一個名為tv-network-list.ts的component, 並且在app模組進行註冊, 如果不存在components資料夾則建立這個資料夾.

ng g c components/TvNetworkList -m=app

生成檔案如下:

 

 

並且已經在app.module進行了註冊:

然後我們再建立兩個component.

建立TvNetwork表單:

根據文件, 使用下面命令建立一個名為tv-network-form.ts的component, 並且在app模組進行註冊, 如果不存在components資料夾則建立這個資料夾.

ng g c components/TvNetworkForm -m=app

上面這個命令使用的都是縮寫. 完整的寫法如下:

ng generate component components/TvNetworkForm --module=app

生成的檔案如下:

再建立一個home component:

ng g c components/home -m=app 

那麼, 如何訪問這個form? 這就需要建立路由了, 不過首先先把bootstrap 4 安裝上, 專案根目錄執行以下命令:

npm install --save bootstrap jquery popper.js

 安裝好之後, 需要把bootstrap的css檔案新增到angular-cli.json檔案裡:

下面新增導航欄, 請參考bootstrap4文件: http://getbootstrap.com/docs/4.0/components/navbar/

修改app.component.html如下:

<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
  <a class="navbar-brand" href="#">Tv</a>
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent"
    aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>

  <div class="collapse navbar-collapse" id="navbarSupportedContent">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active">
        <a class="nav-link" href="#">Home
          <span class="sr-only">(current)</span>
        </a>
      </li>
    </ul>
  </div>
</nav>

然後執行npm start, 結果如下圖就說明bootstrap4安裝好了:

建立angular 路由:

參考官方文件: https://angular.io/tutorial/toh-pt5

執行命令:

ng g m appRouting -flat -m=app

這會建立一個app-routing.module.ts模組, 並且不會建立自己的資料夾, 同樣也會註冊到app模組.

修改app-routing到程式碼如下:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './components/home/home.component';
import { TvNetworkFormComponent } from './components/tv-network-form/tv-network-form.component';
import { TvNetworkListComponent } from './components/tv-network-list/tv-network-list.component';

const ROUTES: Routes = [
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: 'home', component: HomeComponent },
  { path: 'tvnetworks', component: TvNetworkListComponent },
  { path: 'tvnetworks/new', component: TvNetworkFormComponent },
  { path: '**', component: HomeComponent }
];

@NgModule({
  imports: [ RouterModule.forRoot(ROUTES) ],
  exports: [RouterModule]
})
export class AppRoutingModule { }

 

在編寫angular的ts程式碼時, 由於安裝了angular外掛, 所以智慧提示和自動補全和自動引用都是相當好的.

分別設定了5個路由, 預設路由直接跳轉到home, 如果沒有匹配路由到話也是跳轉到home.

然後需要在app.component.html裡面加上router-outlet, 並修改navbar裡面到連結:

<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
  <a class="navbar-brand" href="#">Tv</a>
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent"
    aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>

  <div class="collapse navbar-collapse" id="navbarSupportedContent">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item">
        <a class="nav-link" routerLinkActive="active" routerLink="/home">Home
          <span class="sr-only">(current)</span>
        </a>
      </li>
      <li class="nav-item">
        <a class="nav-link" routerLinkActive="active" routerLink="/tvnetworks">Tv Network
          <span class="sr-only">(current)</span>
        </a>
      </li>
      <li class="nav-item">
        <a class="nav-link" routerLinkActive="active" routerLink="/tvnetworks/new">Add Tv Network
          <span class="sr-only">(current)</span>
        </a>
      </li>
    </ul>
  </div>
</nav>
<div class="container">
  <router-outlet></router-outlet>
</div>

 

檢視瀏覽器, 應該是這個效果:

建立Service

為了使用asp.net core到web api, 需要在angular客戶端建立http的service. 這裡我使用HttpClient.

首先在app.module裡面新增引用:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';


import { AppComponent } from './app.component';
import { TvNetworkFormComponent } from './components/tv-network-form/tv-network-form.component';
import { HomeComponent } from './components/home/home.component';
import { AppRoutingModule } from './/app-routing.module';


@NgModule({
  declarations: [
    AppComponent,
    TvNetworkFormComponent,
    HomeComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

然後使用命令生成service:

ng g s services/TvNetwork -m=app

然後編輯tv-network.service.ts, 新增一個獲得所有tv network的方法, 返回型別是Observable:

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';

@Injectable()
export class TvNetworkService {

  constructor(
    private http: HttpClient
  ) { }

  getTvNetworks () {
    return this.http.get<any[]>('api/tvnetworks');
  }

}

 

隨後我們在tv-netowrk-list.component.ts裡的ngOnInit方法呼叫它, 並把結果列印出來:

import { Component, OnInit } from '@angular/core';
import { TvNetworkService } from '../../services/tv-network.service';

@Component({
  selector: 'app-tv-network-list',
  templateUrl: './tv-network-list.component.html',
  styleUrls: ['./tv-network-list.component.css']
})
export class TvNetworkListComponent implements OnInit {

  tvNetworks: any[];

  constructor(
    private tvNetworkServices: TvNetworkService
  ) { }

  ngOnInit() {
    this.tvNetworkServices.getTvNetworks().subscribe(result => {
      this.tvNetworks = result;
      console.log(this.tvNetworks);
    }, err => {
      console.error(err);
    });
  }

}

 

然後讓我們執行試試:

可以看到發生了錯誤404, angular客戶端並沒有找到這個api. 這是因為angular執行的是自己的web伺服器埠4200, 而asp.net core也是執行自己伺服器埠為5000.

那麼可以有多種解決辦法:

1. 可以在angular的service的url寫成完整的地址, 但是, 由於開發時和生產時的api地址很有可能不一樣, 那麼這就意味著釋出到正式環境之前要把所有services的url地址全部修改一遍, 顯然, 這時不可取的. (也許可以定義一個字首變數, 隨著環境改變它的值).
2. 由於angular cli其實使用的是webpack, 那麼就可以使用proxy. 

我們就使用proxy, 參考官方文件: https://github.com/angular/angular-cli/wiki/stories-proxy

在專案根目錄建立一個proxy.conf.json檔案:

{
  "/api": {
    "target": "http://localhost:5000",
    "secure": false
  }
}

 

這表示所有的以/api開頭的請求將會被轉發到http://localhost:5000/api這個地址上.

此外還需要修改package.json裡面到npm start部分, 把上面的proxy檔案新增為引數:

然後重新執行angular專案, 這時只能使用 npm start這個命令, 如果想使用ng serve 命令則必須把後邊的引數加上.

重新訪問TvNetworks選單:

這次讀取api成功了. 那麼接下來我們來完成這個列表頁面.

cmd+p, 輸入 tv list html 開啟tv-network-list.component.html.

這裡需要畫一個table, 別忘了使用zencoding.

表頭部分, 按照下面輸入然後按Tab:

Tbody部分:

最後程式碼:

<table class="table">
  <thead class="thead-dark">
    <tr>
      <th scope="col">#</th>
      <th scope="col">名稱</th>
      <th scope="col">操作</th>
    </tr>
  </thead>
  <tbody>
    <tr *ngFor="let t of tvNetworks; let i = index">
      <th scope="row">{{i+1}}</th>
      <td>{{t.name}}</td>
      <td></td>
    </tr>
  </tbody>
</table>

 

執行頁面:

Beautiful.

繼續編寫表單:

開啟tv-network-form.component.html, 請看視訊:

最終程式碼如下:

<h1>新增電視臺</h1>
<form>
  <div class="form-group">
    <label for="name">名稱</label>
    <input type="text" name="name" id="name" class="form-control">
  </div>
  <button class="btn btn-primary">提交</button>
</form> 

效果如圖:

如果您跟著這兩篇文章做到現在, 肯定可以感覺到vscode到強大和不同, 它絕不僅僅是個編輯器. 我一直在使用vscode編寫前臺和python等, 現在也習慣使用vscode編寫.net core專案了, Awesome.

今天先寫到這, 下一篇是CRUD部分. 

 

相關文章