目录
介绍
备注
背景
推定(Presumptions)
使用代码
步骤0:将NuGet package WebApiClientGen和WebApiClientGen.jQuery安装到Web API项目
步骤1:准备JSON配置数据
步骤2:运行Web API项目的DEBUG构建和POST JSON配置数据以触发客户端API代码的生成
发布客户端API库
内部用法
外部用法
兴趣点
- GitHub上的代码示例
用于开发ASP.NET Web API或ASP.NET Core Web API的客户端程序, 强类型客户端API生成器以C#代码和TypeScript代码生成强类型客户端API,以最大程度地减少重复性任务并提高应用程序开发人员的生产率和产品质量。然后,您可以向自己以及团队或B2B合作伙伴中的其他开发人员提供或发布所生成的源代码或已编译的客户端API库。
该项目提供以下产品:
- C#中用于强类型客户端API的代码生成器,支持桌面,Universal Windows,Android和iOS。
- 用于jQuery,Angular 2+和Aurelia的TypeScript以及使用Axios的 TypeScript / JavaScript应用程序中的强类型客户端API的代码生成器。
- TypeScript CodeDOM,一种TypeScript的CodeDOM组件,从.NET Framework的CodeDOM派生而来。
- POCO2TS.exe,这是一个从POCO类生成TypsScript接口的命令行程序。
- Fonlow.Poco2Ts,从POCO类生成TypsScript接口的组件
本文重点介绍为jQuery生成TypeScript客户端API。
备注自2016年6月WebApiClientGen v1.9.0-beta 版本起,Angular2的支持就一直可用,当时Angular 2仍在RC1中。从WebApiClientGen v2.0 版本开始支持Angular 2生产版本。
即使您正在执行JavaScript编程,仍可以使用WebApiClientGen,因为可以将生成的TypeScript文件编译为JavaScript文件。尽管您不会获得设计时类型检查和编译时类型检查,但是只要您的JS编辑器支持这种功能,您仍然可以享受源代码文档对设计时的智能感知。
背景如果您曾经使用WCF开发了基于SOAP的Web服务,则可能会喜欢使用由SvcUtil.exe生成的客户端API代码或Visual Studio IDE的Web服务引用。转向Web API时,我感到自己回到了石器时代,因为在设计时我必须利用我的宝贵脑力进行大量数据类型检查,而计算机应该完成这项工作。
早在2010年,我就在IHttpHandler/IHttpModule 的基础上开发了一些RESTful Web服务,这些服务不需要强类型数据,而需要文档和流之类的任意数据。但是,我已经获得了更多具有复杂业务逻辑和数据类型的Web项目,并且我将在整个SDLC中使用高度抽象和语义的数据类型。
我看到ASP.NET Web API确实通过类ApiController支持高度抽象和强类型化的函数原型,并且ASP.NET MVC框架可选地提供生成良好的描述API函数的帮助页面。但是,在开发了Web API之后,我不得不手工制作一些非常原始且重复的客户端代码来使用Web服务。如果Web API是由其他人开发的,那么我必须阅读在线帮助页面,然后进行编写。
因此,我进行了搜索并试图找到一些解决方案,这些解决方案可以使我摆脱编写原始代码和重复代码的麻烦,因此我可以专注于在客户端上基于更高技术抽象的业务逻辑构建。以下是协助客户程序开发的开源项目列表:
- WADL
- RAML with .NET
- WebApiProxy
- Swashbuckle
- AutoRest
- OData
- TypeLITE
- TypeWriter
虽然这些解决方案可以生成强类型的客户端代码并在某种程度上减少重复的任务,但我发现它们都无法给我带来我期望的所有高效编程经验:
- 映射到服务数据模型的强类型客户端数据模型。
- 强类型函数原型映射到的派生类ApiController的功能。
- 像WCF编程那样以批发方式生成代。
- 使用流行的属性(如DataContractAttribute和JsonObjectAttribute等)通过数据注释来挑选数据模型。
- 在设计时和编译时进行类型检查。
- 用于客户端数据模型,功能原型和文档注释的智能感知。
这是WebApiClientGen。
推定(Presumptions)- 您将开发ASP.NET Web API 2.x应用程序,并将开发基于jQuery或SPA和Angular2的基于AJAX的Web前端的JavaScript库。
- 您和开发人员都喜欢通过服务器端和客户端中的强类型函数来实现高度抽象,并且使用TypeScript。
- Web API和Entity Framework Code First都使用POCO类,并且您可能不想将所有数据类和成员发布到客户端程序。
并且,可选地,如果您或您的团队认可基于Trunk的开发会更好,因为WebApiClientGen的设计和使用 WebApiClientGen的工作流程正在考虑基于Trunk的开发,与其他分支策略(如功能分支和Gitflow等)相比,这种方法对于持续集成更为有效。
为了跟进这种开发客户端程序的新方法,最好拥有一个ASP.NET Web API项目或一个包含Web API的MVC项目。您可以使用现有项目,也可以创建一个演示项目。
使用代码本文重点介绍jQuery的代码示例。
步骤0:将NuGet package WebApiClientGen和WebApiClientGen.jQuery安装到Web API项目安装还将安装相关的NuGet软件包Fonlow.TypeScriptCodeDOM和Fonlow.Poco2Ts项目引用。
HttpClient的助手库 应与生成的代码一起复制到Scripts文件夹中,这些代码将在每次执行CodeGen时更新。
此外,用于触发CodeGen的CodeGenController.cs已添加到项目的Controllers文件夹中。
CodeGenController选项仅在调试版本的开发过程中可用,因为应该为每个版本的Web API生成一次客户端API。
#if DEBUG //This controller is not needed in production release,
// since the client API should be generated during development of the Web Api.
...
namespace Fonlow.WebApiClientGen
{
[System.Web.Http.Description.ApiExplorerSettings(IgnoreApi = true)]//this controller is a
//dev backdoor during development, no need to be visible in ApiExplorer.
public class CodeGenController : ApiController
{
///
/// Trigger the API to generate WebApiClientAuto.cs for an established client API project.
/// POST to http://localhost:10965/api/CodeGen with json object CodeGenParameters
///
///
/// OK if OK
[HttpPost]
public string TriggerCodeGen(CodeGenParameters parameters)
{
...
}
}
备注
- CodeGenController已安装在YourMvcOrWebApiProject/Controllers中,即使MVC项目的脚手架可能具有用于ApiController的派生类的文件夹API 。但是,通常最好在独立的Web API项目中实现Web API。并且,如果您希望MVC项目和Web API项目在同一网站中运行,则可以将Web API作为MVC网站的应用程序进行安装。
- WebApiClientGenCore不安装CodeGenController,您应该复制该文件。
启用Web API的文档注释
在C:\YourWebSlnPath\Your.WebApi\Areas\HelpPage\App_Start\HelpPageConfig.cs中,有以下行:
//config.SetDocumentationProvider(new XmlDocumentationProvider
(HttpContext.Current.Server.MapPath("~/App_Data/XmlDocument.xml")));
取消注释,使其像这样:
config.SetDocumentationProvider(new XmlDocumentationProvider
(HttpContext.Current.Server.MapPath("~/bin/Your.WebApi.xml")));
在项目“属性”页面的“Build”选项卡中,检查“输出/XML文档文件”并设置“bin\Your.WebApi.xml”,而输出路径默认情况下为“bin”。
如果您有用于数据模型的其他程序集,则可以执行相同的操作以确保生成文档注释并将其复制到客户端API。
步骤1:准备JSON配置数据您的Web API项目可能具有POCO类和类似blow的API函数。[ 数据模型和ApiController的完整代码示例]
namespace DemoWebApi.DemoData
{
public sealed class Constants
{
public const string DataNamespace = "http://fonlow.com/DemoData/2014/02";
}
[DataContract(Namespace = Constants.DataNamespace)]
public enum AddressType
{
[EnumMember]
Postal,
[EnumMember]
Residential,
};
[DataContract(Namespace = Constants.DataNamespace)]
public enum Days
{
[EnumMember]
Sat = 1,
[EnumMember]
Sun,
[EnumMember]
Mon,
[EnumMember]
Tue,
[EnumMember]
Wed,
[EnumMember]
Thu,
[EnumMember]
Fri
};
[DataContract(Namespace = Constants.DataNamespace)]
public class Address
{
[DataMember]
public Guid Id { get; set; }
public Entity Entity { get; set; }
///
/// Foreign key to Entity
///
public Guid EntityId { get; set; }
[DataMember]
public string Street1 { get; set; }
[DataMember]
public string Street2 { get; set; }
[DataMember]
public string City { get; set; }
[DataMember]
public string State { get; set; }
[DataMember]
public string PostalCode { get; set; }
[DataMember]
public string Country { get; set; }
[DataMember]
public AddressType Type { get; set; }
[DataMember]
public DemoWebApi.DemoData.Another.MyPoint Location;
}
[DataContract(Namespace = Constants.DataNamespace)]
public class Entity
{
public Entity()
{
Addresses = new List();
}
[DataMember]
public Guid Id { get; set; }
[DataMember(IsRequired =true)]//MVC and Web API does not care
[System.ComponentModel.DataAnnotations.Required]//MVC and Web API care about only this
public string Name { get; set; }
[DataMember]
public IList Addresses { get; set; }
public override string ToString()
{
return Name;
}
}
[DataContract(Namespace = Constants.DataNamespace)]
public class Person : Entity
{
[DataMember]
public string Surname { get; set; }
[DataMember]
public string GivenName { get; set; }
[DataMember]
public DateTime? BirthDate { get; set; }
public override string ToString()
{
return Surname + ", " + GivenName;
}
}
[DataContract(Namespace = Constants.DataNamespace)]
public class Company : Entity
{
[DataMember]
public string BusinessNumber { get; set; }
[DataMember]
public string BusinessNumberType { get; set; }
[DataMember]
public string[][] TextMatrix
{ get; set; }
[DataMember]
public int[][] Int2DJagged;
[DataMember]
public int[,] Int2D;
[DataMember]
public IEnumerable Lines;
}
...
...
namespace DemoWebApi.Controllers
{
[RoutePrefix("api/SuperDemo")]
public class EntitiesController : ApiController
{
///
/// Get a person
///
/// unique id of that guy
/// person in db
[HttpGet]
public Person GetPerson(long id)
{
return new Person()
{
Surname = "Huang",
GivenName = "Z",
Name = "Z Huang",
BirthDate = DateTime.Now.AddYears(-20),
};
}
[HttpPost]
public long CreatePerson(Person p)
{
Debug.WriteLine("CreatePerson: " + p.Name);
if (p.Name == "Exception")
throw new InvalidOperationException("It is exception");
Debug.WriteLine("Create " + p);
return 1000;
}
[HttpPut]
public void UpdatePerson(Person person)
{
Debug.WriteLine("Update " + person);
}
[HttpPut]
[Route("link")]
public bool LinkPerson(long id, string relationship, [FromBody] Person person)
{
return person != null && !String.IsNullOrEmpty(relationship);
}
[HttpDelete]
public void Delete(long id)
{
Debug.WriteLine("Delete " + id);
}
[Route("Company")]
[HttpGet]
public Company GetCompany(long id)
{
以下JSON配置数据将POST到CodeGen Web API:
{
"ApiSelections": {
"ExcludedControllerNames": [
"DemoWebApi.Controllers.Account",
"DemoWebApi.Controllers.FileUpload"
],
"DataModelAssemblyNames": [
"DemoWebApi.DemoData",
"DemoWebApi"
],
"CherryPickingMethods": 3
},
"ClientApiOutputs": {
"ClientLibraryProjectFolderName": "..\\DemoWebApi.ClientApi",
"GenerateBothAsyncAndSync": true,
"CamelCase": true,
"Plugins": [
{
"AssemblyName": "Fonlow.WebApiClientGen.jQuery",
"TargetDir": "Scripts\\ClientApi",
"TSFile": "WebApiJQClientAuto.ts",
"AsModule": false,
"ContentType": "application/json;charset=UTF-8"
},
{
"AssemblyName": "Fonlow.WebApiClientGen.NG2",
"TargetDir": "..\\DemoNGCli\\NGSource\\src\\ClientApi",
"TSFile": "WebApiNG2ClientAuto.ts",
"AsModule": true,
"ContentType": "application/json;charset=UTF-8"
}
]
}
}
建议将JSON配置数据保存到Web API项目文件夹中的此类文件中。
如果在Web API项目中定义了所有POCO类,则应将Web API项目的程序集名称放在“DataModelAssemblyNames”数组中。如果您有一些专用的数据模型程序集可以很好地分离关注点,则应将相应的程序集名称放入数组中。
“TypeScriptNG2Folder”是Angular2项目的绝对路径或相对路径。例如,“..\\DemoAngular2\\ClientApi”表示作为Web API项目的同级项目创建的Angular 2项目。
CodeGen根据“CherryPickingMethods” 从POCO类生成强类型TypeScript接口,这在下面的文档注释中进行了描述:
///
/// Flagged options for cherry picking in various development processes.
///
[Flags]
public enum CherryPickingMethods
{
///
/// Include all public classes, properties and properties.
///
All = 0,
///
/// Include all public classes decorated by DataContractAttribute,
/// and public properties or fields decorated by DataMemberAttribute.
/// And use DataMemberAttribute.IsRequired
///
DataContract =1,
///
/// Include all public classes decorated by JsonObjectAttribute,
/// and public properties or fields decorated by JsonPropertyAttribute.
/// And use JsonPropertyAttribute.Required
///
NewtonsoftJson = 2,
///
/// 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.
///
Serializable = 4,
///
/// Include all public classes, properties and properties.
/// And use System.ComponentModel.DataAnnotations.RequiredAttribute.
///
AspNet = 8,
}
默认值是opt-in的DataContract。您可以使用任何一种方法或方法的组合。
在IIS Express上的IDE中运行Web项目。
然后使用 Cur或 Poster或任何你喜欢的客户端工具,将带有content-type=application/json的POST到http://localhost:10965/api/CodeGen。
提示
因此,基本上,每当更新Web API时,您仅需要步骤2即可生成客户端API,因为您不需要每次都安装NuGet包或制作新的JSON配置数据。
编写一些批处理脚本来启动Web API和POST JSON配置数据应该不难。为了方便起见,我实际上起草了一个:Powershell脚本文件,该文件在IIS Express上启动Web(API)项目,然后发布JSON配置文件以触发代码生成。
发布客户端API库现在,您已经在TypeScript中生成了客户端API,类似于以下示例:
///
///
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 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;
}
export interface Person extends DemoWebApi_DemoData_Client.Entity {
Surname?: string;
GivenName?: string;
BirthDate?: Date;
}
export interface Company extends DemoWebApi_DemoData_Client.Entity {
BusinessNumber?: string;
BusinessNumberType?: string;
TextMatrix?: Array;
Int3D?: Array;
Lines?: Array;
}
}
namespace DemoWebApi_DemoData_Another_Client {
export interface MyPoint {
X?: number;
Y?: number;
}
}
namespace DemoWebApi_Controllers_Client {
export class Entities {
httpClient: HttpClient;
constructor(public baseUri?: string, public error?:
(xhr: JQueryXHR, ajaxOptions: string, thrown: string) =>
any, public statusCode?: { [key: string]: any; }){
this.httpClient = new HttpClient();
}
/**
* Get a person
* GET api/Entities/{id}
* @param {number} id unique id of that guy
* @return {DemoWebApi_DemoData_Client.Person} person in db
*/
GetPerson(id: number, callback: (data : DemoWebApi_DemoData_Client.Person) => any){
this.httpClient.get(encodeURI(this.baseUri +
'api/Entities/'+id), callback, this.error, this.statusCode);
}
/**
* POST api/Entities
* @param {DemoWebApi_DemoData_Client.Person} person
* @return {number}
*/
CreatePerson(person: DemoWebApi_DemoData_Client.Person,
callback: (data : number) => any){
this.httpClient.post(encodeURI(this.baseUri +
'api/Entities'), person, callback, this.error, this.statusCode);
}
/**
* PUT api/Entities
* @param {DemoWebApi_DemoData_Client.Person} person
* @return {void}
*/
UpdatePerson(person: DemoWebApi_DemoData_Client.Person, callback: (data : void) => any){
this.httpClient.put(encodeURI(this.baseUri +
'api/Entities'), person, callback, this.error, this.statusCode);
}
/**
* DELETE api/Entities/{id}
* @param {number} id
* @return {void}
*/
Delete(id: number, callback: (data : void) => any){
this.httpClient.delete(encodeURI(this.baseUri +
'api/Entities/'+id), callback, this.error, this.statusCode);
}
}
export class Values {
httpClient: HttpClient;
constructor(public baseUri?: string, public error?:
(xhr: JQueryXHR, ajaxOptions: string, thrown: string) => any,
public statusCode?: { [key: string]: any; }){
this.httpClient = new HttpClient();
}
/**
* GET api/Values
* @return {Array}
*/
Get(callback: (data : Array) => any){
this.httpClient.get(encodeURI(this.baseUri +
'api/Values'), callback, this.error, this.statusCode);
}
/**
* GET api/Values/{id}?name={name}
* @param {number} id
* @param {string} name
* @return {string}
*/
GetByIdAndName(id: number, name: string, callback: (data : string) => any){
this.httpClient.get(encodeURI(this.baseUri +
'api/Values/'+id+'?name='+name),
callback, this.error, this.statusCode);
}
/**
* POST api/Values
* @param {string} value
* @return {string}
*/
Post(value: {'':string}, callback: (data : string) => any){
this.httpClient.post(encodeURI(this.baseUri +
'api/Values'), value, callback, this.error, this.statusCode);
}
/**
* PUT api/Values/{id}
* @param {number} id
* @param {string} value
* @return {void}
*/
Put(id: number, value: {'':string}, callback: (data : void) => any){
this.httpClient.put(encodeURI(this.baseUri +
'api/Values/'+id), value, callback, this.error, this.statusCode);
}
/**
* DELETE api/Values/{id}
* @param {number} id
* @return {void}
*/
Delete(id: number, callback: (data : void) => any){
this.httpClient.delete(encodeURI(this.baseUri +
'api/Values/'+id), callback, this.error, this.statusCode);
}
}
}
提示
1.如果希望生成的TypeScript代码符合JavaScript和JSON的驼峰式格式,则可以在Web API的脚手架代码类WebApiConfig中添加以下行:
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver =
new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();
如果C#中的相应名称是Pascal式格式,则属性名称和函数名称将是驼峰式格式。有关更多详细信息,请检查camelCasing或PascalCasing。
2. PowerShell脚本还将TS文件编译为JS文件。
内部用法
在像Visual Studio这样的不错的文本编辑器中编写客户端代码时,您可能会得到很好的智能感知。
如果希望某些外部开发人员通过JavaScript使用Web API,则可以发布生成的TypeScript客户端API文件或已编译的JavaScript文件,以及ASP.NET MVC框架生成的帮助页面。
兴趣点虽然ASP.NET MVC和Web API将NewtonSoft.Json用于JSON应用程序,但NewtonSoft.Json可以很好地处理由DataContractAttribute修饰的POCO类。
通过将dot替换为下划线并添加“Client”作为后缀,可以将CLR命名空间转换为TypeScript命名空间。例如,namespace My.Name.space将被翻译为My_Name_space_Client。
从某种角度来看,服务命名空间/函数名与客户端命名空间/函数名之间的一对一映射公开了服务的实现细节,这通常是不推荐的。但是,传统的RESTful客户端编程要求程序员注意服务功能的URL查询模板,并且查询模板包含服务的实现细节。因此,这两种方法都会在某种程度上公开服务的实现细节,但会带来不同的后果。
对于客户开发人员,经典的函数原型如下:
ReturnType DoSomething(Type1 t1, Type2 t2 ...)
是API函数,其余是传输的技术实现细节:TCP / IP,HTTP,SOAP,面向资源,基于CRUD的URI,RESTful,XML和JSON等。函数原型和一段API文档应该足以调用API函数。客户开发人员至少在操作成功时就不必关心传输的那些实现细节。仅当出现错误时,开发人员才需要关心技术细节。例如,在基于SOAP的Web服务中,您必须了解SOAP错误。在RESTful Web服务中,您可能必须处理HTTP状态代码和响应。
而且查询模板几乎没有提供API函数的语义含义。相比之下,WebApiClientGen以服务端函数命名客户端函数,就像WCF中的SvcUtil.exe在默认情况下所做一样,因此,只要服务开发人员以良好的语义名称命名服务函数,生成的客户端函数就具有良好的语义。
在涵盖服务开发和客户端开发的SDLC全景图中,服务开发人员具有服务函数的语义含义,通常在功能描述之后命名函数是一种良好的编程习惯。面向资源的CRUD可能具有语义含义,或者仅仅是功能描述的技术翻译。
WebApiClientGen将Web API的文档注释复制到生成的TypeScript代码中的JsDoc3注释中,因此您几乎不需要阅读MVC生成的帮助页面,并且使用该服务的客户端编程将变得更加无缝。
许多JavaScript框架(如React和Vue.js)没有内置的HTTP请求库,而是依赖于第3方库(如Axios)。由于Axios显然是近年来在JavaScript程序员中最受欢迎的一个,由Reavy和Vuu.js推荐,因此支持Axios可能更为可行。
提示
对于持续集成,编写脚本以完全自动化某些步骤并不难。您可以在以下位置找到示例
- WebApiClientGen
- WebApiClientGen示例
- .NET Core演示
并在根文件夹中找到那些“Create * ClientApi.ps1”文件。