本文介绍“ 为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项目。

然后使用curlposter或任何您喜欢的客户端工具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而已!webapiclientgencreateclientapi.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+。