1. 程式人生 > >使用ASP.NET Web API和Web API Client Gen使Angular 2應用程式的開發更加高效

使用ASP.NET Web API和Web API Client Gen使Angular 2應用程式的開發更加高效

本文介紹“ 為ASP.NET Web API生成TypeScript客戶端API ”,重點介紹Angular 2+程式碼示例和各自的SDLC。如果您正在開發.NET Core Web API後端,則可能需要閱讀為ASP.NET Core Web API生成C#Client API。

背景

WebApiClientGenAngular 2仍然在RC2時,自2016年6月v1.9.0-beta 以來,對Angular2的支援已經可用。並且在WebApiClientGenv2.0中提供了對Angular 2產品釋出的支援。希望NG2的發展不會如此頻繁地破壞我的CodeGen和我的Web前端應用程式。:)

在2016年9月底釋出Angular 2的第一個產品釋出幾周後,我碰巧啟動了一個使用Angular2的主要Web應用程式專案,因此我WebApiClientGen對NG2應用程式開發的使用方法幾乎相同。

推定

  1. 您正在開發ASP.NET Web API 2.x應用程式,並將基於Angular 2+為SPA開發TypeScript庫。
  2. 您和其他開發人員喜歡在伺服器端和客戶端都通過強型別資料和函式進行高度抽象。
  3. Web API和實體框架程式碼優先使用POCO類,您可能不希望將所有資料類和成員釋出到客戶端程式原始碼。

並且可選地,如果您或您的團隊支援基於Trunk的開發,那麼更好,因為使用的設計WebApiClientGen

和工作流程WebApiClientGen假設基於Trunk的開發,這比其他分支策略(如Feature Branching和GitFlow等)更有效。對於熟練掌握TDD的團隊。

為了跟進這種開發客戶端程式的新方法,最好有一個ASP.NET Web API專案。您可以使用現有專案,也可以建立演示專案。

使用程式碼

本文重點介紹Angular 2+的程式碼示例。假設您有一個ASP.NET Web API專案和一個Angular2專案作為VS解決方案中的兄弟專案。如果你將它們分開,那麼為了使開發步驟無縫地編寫指令碼應該不難。

我認為您已閱讀“ 為ASP.NET Web API生成TypeScript客戶端API ”。為jQuery生成客戶端API的步驟幾乎與為Angular 2生成客戶端API的步驟相同。演示TypeScript程式碼基於TUTORIAL:TOUR OF HEROES,許多人從中學習了Angular2。因此,您將能夠看到如何WebApiClientGen

適應並改進Angular2應用程式的典型開發週期。

這是Web API程式碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using System.Runtime.Serialization;
using System.Collections.Concurrent;

namespace DemoWebApi.Controllers
{
    [RoutePrefix("api/Heroes")]
    public class HeroesController : ApiController
    {
        public Hero[] Get()
        {
            return HeroesData.Instance.Dic.Values.ToArray();
        }

        public Hero Get(long id)
        {
            Hero r;
            HeroesData.Instance.Dic.TryGetValue(id, out r);
            return r;
        }

        public void Delete(long id)
        {
            Hero r;
            HeroesData.Instance.Dic.TryRemove(id, out r);
        }

        public Hero Post(string name)
        {
            var max = HeroesData.Instance.Dic.Keys.Max();
            var hero = new Hero { Id = max + 1, Name = name };
            HeroesData.Instance.Dic.TryAdd(max + 1, hero);
            return hero;
        }

        public Hero Put(Hero hero)
        {
            HeroesData.Instance.Dic[hero.Id] = hero;
            return hero;
        }

        [HttpGet]
        public Hero[] Search(string name)
        {
            return HeroesData.Instance.Dic.Values.Where(d => d.Name.Contains(name)).ToArray();
        }          
    }

    [DataContract(Namespace = DemoWebApi.DemoData.Constants.DataNamespace)]
    public class Hero
    {
        [DataMember]
        public long Id { get; set; }

        [DataMember]
        public string Name { get; set; }
    }

    public sealed class HeroesData
    {
        private static readonly Lazy<HeroesData> lazy =
            new Lazy<HeroesData>(() => new HeroesData());

        public static HeroesData Instance { get { return lazy.Value; } }

        private HeroesData()
        {
            Dic = new ConcurrentDictionary<long, Hero>(new KeyValuePair<long, Hero>[] {
                new KeyValuePair<long, Hero>(11, new Hero {Id=11, Name="Mr. Nice" }),
                new KeyValuePair<long, Hero>(12, new Hero {Id=12, Name="Narco" }),
                new KeyValuePair<long, Hero>(13, new Hero {Id=13, Name="Bombasto" }),
                new KeyValuePair<long, Hero>(14, new Hero {Id=14, Name="Celeritas" }),
                new KeyValuePair<long, Hero>(15, new Hero {Id=15, Name="Magneta" }),
                new KeyValuePair<long, Hero>(16, new Hero {Id=16, Name="RubberMan" }),
                new KeyValuePair<long, Hero>(17, new Hero {Id=17, Name="Dynama" }),
                new KeyValuePair<long, Hero>(18, new Hero {Id=18, Name="Dr IQ" }),
                new KeyValuePair<long, Hero>(19, new Hero {Id=19, Name="Magma" }),
                new KeyValuePair<long, Hero>(20, new Hero {Id=29, Name="Tornado" }),

                });
        }

        public ConcurrentDictionary<long, Hero> Dic { get; private set; }
    }
}

 

步驟0:將NuGet包WebApiClientGen安裝到Web API專案

安裝還將安裝依賴的NuGet包Fonlow.TypeScriptCodeDOMFonlow.Poco2Ts專案引用。

此外,用於觸發CodeGen的CodeGenController.cs被新增到Web API專案的Controllers資料夾中。

CodeGenController只在除錯版本開發過程中應該是可用的,因為客戶端API應該用於Web API的每個版本生成一次。

提示

如果您正在使用@ angular / http中定義的Angular2的Http服務,那麼您應該使用WebApiClientGenv2.2.5。如果您使用的HttpClient是@ angular / common / http中定義的Angular 4.3中可用的服務,並且在Angular 5中已棄用,那麼您應該使用WebApiClientGenv2.3.0。

第1步:準備JSON配置資料

下面的JSON配置資料是POSTCodeGen Web API:

{
    "ApiSelections": {
        "ExcludedControllerNames": [
            "DemoWebApi.Controllers.Account"
        ],

        "DataModelAssemblyNames": [
            "DemoWebApi.DemoData",
            "DemoWebApi"
        ],
        "CherryPickingMethods": 1
    },

    "ClientApiOutputs": {
        "ClientLibraryProjectFolderName": "DemoWebApi.ClientApi",
        "GenerateBothAsyncAndSync": true,

        "CamelCase": true,
        "TypeScriptNG2Folder": "..\\DemoAngular2\\clientapi",
        "NGVersion" : 5

    }
}

 

提示

Angular 6正在使用RxJS v6,它引入了一些重大變化,特別是對於匯入Observable。預設情況下,WebApiClientGen2.4和更高版本預設將匯入宣告為import { Observable } from 'rxjs';  。如果您仍在使用Angular 5.x,則需要"NGVersion" : 5在JSON配置中宣告,因此生成的程式碼中的匯入將是更多詳細資訊,import { Observable } from 'rxjs/Observable'; . 請參閱RxJS v5.x至v6更新指南和RxJS:版本6的TSLint規則。

備註

您應確保“ TypeScriptNG2Folder”存在的資料夾存在,因為WebApiClientGen不會為您建立此資料夾,這是設計使然。

建議到JSON配置資料儲存到與檔案類似的這一個位於Web API專案資料夾。

如果您在Web API專案中定義了所有POCO類,則應將Web API專案的程式集名稱放在“ DataModelAssemblyNames” 陣列中。如果您有一些專用的資料模型程式集可以很好地分離關注點,那麼您應該將相應的程式集名稱放入陣列中。您可以選擇為jQuery或NG2或C#客戶端API程式碼生成TypeScript客戶端API程式碼,或者全部三種。

“ TypeScriptNG2Folder”是Angular2專案的絕對路徑或相對路徑。例如,“ .. \\ DemoAngular2 \\ ClientApi ”表示DemoAngular2作為Web API專案的兄弟專案建立的Angular 2專案“ ”。

CodeGen根據“從POCO類生成強型別打字稿介面CherryPickingMethods,其在下面的文件註釋描述”:

/// <summary>
/// Flagged options for cherry picking in various development processes.
/// </summary>
[Flags]
public enum CherryPickingMethods
{
    /// <summary>
    /// Include all public classes, properties and properties.
    /// </summary>
    All = 0,

    /// <summary>
    /// Include all public classes decorated by DataContractAttribute,
    /// and public properties or fields decorated by DataMemberAttribute.
    /// And use DataMemberAttribute.IsRequired
    /// </summary>
    DataContract =1,

    /// <summary>
    /// Include all public classes decorated by JsonObjectAttribute,
    /// and public properties or fields decorated by JsonPropertyAttribute.
    /// And use JsonPropertyAttribute.Required
    /// </summary>
    NewtonsoftJson = 2,

    /// <summary>
    /// Include all public classes decorated by SerializableAttribute,
    /// and all public properties or fields
    /// but excluding those decorated by NonSerializedAttribute.
    /// And use System.ComponentModel.DataAnnotations.RequiredAttribute.
    /// </summary>
    Serializable = 4,

    /// <summary>
    /// Include all public classes, properties and properties.
    /// And use System.ComponentModel.DataAnnotations.RequiredAttribute.
    /// </summary>
    AspNet = 8,
}

 

預設選項是DataContract選擇加入。您可以使用任何方法或組合方法。

第2步:執行Web API專案的DEBUG構建

步驟3:POST JSON配置資料以觸發客戶端API程式碼的生成

在IIS Express上的IDE中執行Web專案。

然後使用Curl或Poster或任何您喜歡的客戶端工具POST到http:// localhost:10965 / api / CodeGen,with content-type=application/json

提示

基本上,每當Web API更新時,您只需要步驟2來生成客戶端API,因為您不需要每次都安裝NuGet包或建立新的JSON配置資料。

編寫一些批處理指令碼來啟動Web API和POST JSON配置資料應該不難。為了您的方便,我實際起草了一個:Powershell指令碼檔案CreateClientApi.ps1,它在IIS Express上啟動Web(API)專案,然後釋出JSON配置檔案以觸發程式碼生成。

基本上,您可以製作Web API程式碼,包括API控制器和資料模型,然後執行CreateClientApi.ps1。而已!WebApiClientGen和CreateClientApi.ps1將為您完成剩下的工作。

釋出客戶端API庫

現在您在TypeScript中生成了客戶端API,類似於以下示例:

import { Injectable, Inject } from '@angular/core';
import { Http, Headers, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
export namespace DemoWebApi_DemoData_Client {
    export enum AddressType {Postal, Residential}

    export enum Days {Sat=1, Sun=2, Mon=3, Tue=4, Wed=5, Thu=6, Fri=7}

    export interface PhoneNumber {
        fullNumber?: string;
        phoneType?: DemoWebApi_DemoData_Client.PhoneType;
    }

    export enum PhoneType {Tel, Mobile, Skype, Fax}

    export interface Address {
        id?: string;
        street1?: string;
        street2?: string;
        city?: string;
        state?: string;
        postalCode?: string;
        country?: string;
        type?: DemoWebApi_DemoData_Client.AddressType;
        location?: DemoWebApi_DemoData_Another_Client.MyPoint;
    }

    export interface Entity {
        id?: string;
        name: string;
        addresses?: Array<DemoWebApi_DemoData_Client.Address>;
        phoneNumbers?: Array<DemoWebApi_DemoData_Client.PhoneNumber>;
    }

    export interface Person extends DemoWebApi_DemoData_Client.Entity {
        surname?: string;
        givenName?: string;
        dob?: Date;
    }

    export interface Company extends DemoWebApi_DemoData_Client.Entity {
        businessNumber?: string;
        businessNumberType?: string;
        textMatrix?: Array<Array<string>>;
        int2DJagged?: Array<Array<number>>;
        int2D?: number[][];
        lines?: Array<string>;
    }

    export interface MyPeopleDic {
        dic?: {[id: string]: DemoWebApi_DemoData_Client.Person };
        anotherDic?: {[id: string]: string };
        intDic?: {[id: number]: string };
    }
}

export namespace DemoWebApi_DemoData_Another_Client {
    export interface MyPoint {
        x: number;
        y: number;
    }

}

export namespace DemoWebApi_Controllers_Client {
    export interface FileResult {
        fileNames?: Array<string>;
        submitter?: string;
    }

    export interface Hero {
        id?: number;
        name?: string;
    }
}

   @Injectable()
    export class Heroes {
        constructor(@Inject('baseUri') private baseUri: string = location.protocol + '//' + 
        location.hostname + (location.port ? ':' + location.port : '') + '/', private http: Http){
        }

        /**
         * Get all heroes.
         * GET api/Heroes
         * @return {Array<DemoWebApi_Controllers_Client.Hero>}
         */
        get(): Observable<Array<DemoWebApi_Controllers_Client.Hero>>{
            return this.http.get(this.baseUri + 'api/Heroes').map(response=> response.json());
        }

        /**
         * Get a hero.
         * GET api/Heroes/{id}
         * @param {number} id
         * @return {DemoWebApi_Controllers_Client.Hero}
         */
        getById(id: number): Observable<DemoWebApi_Controllers_Client.Hero>{
            return this.http.get(this.baseUri + 'api/Heroes/'+id).map(response=> response.json());
        }

        /**
         * DELETE api/Heroes/{id}
         * @param {number} id
         * @return {void}
         */
        delete(id: number): Observable<Response>{
            return this.http.delete(this.baseUri + 'api/Heroes/'+id);
        }

        /**
         * Add a hero
         * POST api/Heroes?name={name}
         * @param {string} name
         * @return {DemoWebApi_Controllers_Client.Hero}
         */
        post(name: string): Observable<DemoWebApi_Controllers_Client.Hero>{
            return this.http.post(this.baseUri + 'api/Heroes?name='+encodeURIComponent(name), 
            JSON.stringify(null), { headers: new Headers({ 'Content-Type': 
            'text/plain;charset=UTF-8' }) }).map(response=> response.json());
        }

        /**
         * Update hero.
         * PUT api/Heroes
         * @param {DemoWebApi_Controllers_Client.Hero} hero
         * @return {DemoWebApi_Controllers_Client.Hero}
         */
        put(hero: DemoWebApi_Controllers_Client.Hero): Observable<DemoWebApi_Controllers_Client.Hero>{
            return this.http.put(this.baseUri + 'api/Heroes', JSON.stringify(hero), 
            { headers: new Headers({ 'Content-Type': 'text/plain;charset=UTF-8' 
            }) }).map(response=> response.json());
        }

        /**
         * Search heroes
         * GET api/Heroes?name={name}
         * @param {string} name keyword contained in hero name.
         * @return {Array<DemoWebApi_Controllers_Client.Hero>} Hero array matching the keyword.
         */
        search(name: string): Observable<Array<DemoWebApi_Controllers_Client.Hero>>{
            return this.http.get(this.baseUri + 'api/Heroes?name='+
            encodeURIComponent(name)).map(response=> response.json());
        }
    }

 

提示

如果您希望生成的TypeScript程式碼符合JavaScript和JSON的camel大小寫,則可以在WebApiConfigWeb API的腳手架程式碼類中新增以下行:

config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = 
            new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();

然後屬性名稱和函式名稱將在camel大小寫中,前提是C#中的相應名稱都在Pascal大小寫中。有關詳細資訊,請檢視camelCasing或PascalCasing。

客戶端應用程式設計

在像Visual Studio這樣的正常文字編輯器中編寫客戶端程式碼時,您可能會獲得很好的智慧感知。

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import * as namespaces from '../clientapi/WebApiNG2ClientAuto';
import DemoWebApi_Controllers_Client = namespaces.DemoWebApi_Controllers_Client;

@Component({
    moduleId: module.id,
    selector: 'my-heroes',
    templateUrl: 'heroes.component.html',
    styleUrls: ['heroes.component.css']
})

 

通過IDE進行設計時型別檢查,並在生成的程式碼之上進行編譯時型別檢查,可以更輕鬆地提高客戶端程式設計的效率和產品質量。

不要做計算機可以做的事情,讓計算機為我們努力工作。我們的工作是為客戶提供自動化解決方案,因此最好先自行完成自己的工作。

興趣點

在典型的角2個教程,包括官方的一個  這已經存檔,作者經常督促應用程式開發者製作一個服務類,如“ HeroService”,而黃金法則是:永遠委託給配套服務類的資料訪問。

WebApiClientGen為您生成此服務類DemoWebApi_Controllers_Client.Heroes,它將使用真正的Web API而不是記憶體中的Web API。在開發過程中WebApiClientGen,我建立了一個演示專案DemoAngular2和各自用於測試的Web API控制器。

典型的教程還建議使用模擬服務進行單元測試。WebApiClientGen使用真正的Web API服務要便宜得多,因此您可能不需要建立模擬服務。您應該在開發期間平衡使用模擬或實際服務的成本/收益,具體取決於您的上下文。通常,如果您的團隊已經能夠在每臺開發機器中使用持續整合環境,那麼使用真實服務執行測試可能非常無縫且快速。

在典型的SDLC中,在初始設定之後,以下是開發Web API和NG2應用程式的典型步驟:

  1. 升級Web API
  2. 執行CreateClientApi.ps1以更新TypeScript for NG2中的客戶端API。
  3. 使用生成的TypeScript客戶端API程式碼或C#客戶端API程式碼,在Web API更新時建立新的整合測試用例。
  4. 相應地修改NG2應用程式。
  5. 要進行測試,請執行StartWebApi.ps1以啟動Web API,並在VS IDE中執行NG2應用程式。

提示

對於第5步,有其他選擇。例如,您可以使用VS IDE同時以除錯模式啟動Web API和NG2應用程式。一些開發人員可能更喜歡使用“ npm start”。

本文最初是為Angular 2編寫的,具有Http服務。Angular 4.3中引入了WebApiClientGen2.3.0支援HttpClient。並且生成的API在介面級別保持不變。這使得從過時的Http服務遷移到HttpClient服務相當容易或無縫,與Angular應用程式程式設計相比,不使用生成的API而是直接使用Http服務。

順便說一句,如果你沒有完成向Angular 5的遷移,那麼這篇文章可能有所幫助:  升級到Angular 5和HttpClient。如果您使用的是Angular 6,則應使用WebApiClientGen2.4.0