第一部分: 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部分.