65.9K
CodeProject 正在变化。 阅读更多。
Home

生成 ASP.NET Web API 的 C# 客户端 API

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (69投票s)

2016年1月25日

CPOL

16分钟阅读

viewsIcon

304983

生成强类型 C# 客户端 API,用于 ASP.NET Web API 和 .NET Core Web API,支持桌面、通用 Windows、Android 和 iOS

引言

为了开发 ASP.NET Web APIASP.NET Core Web API 的客户端程序,强类型客户端 API 生成器 以 C# 代码和 TypeScript 代码形式生成强类型客户端 API。该工具旨在最大限度地减少重复性任务,简化后端开发与前端开发之间的协调,并通过更少的投入、工作和压力来提高开发团队的生产力和产品的质量。

这个开源项目提供了以下产品

  1. 强类型 C# 客户端 API 代码生成器,支持桌面、通用 Windows、Android 和 iOS
  2. 强类型 TypeScript 客户端 API 代码生成器,支持 jQuery、Angular 2+、Axios、Aurelia 和 Fetch API。
  3. TypeScript CodeDOM,一个 TypeScript 的 CodeDOM 组件,源自 .NET Framework 的 CodeDOM
  4. POCO2TS.exe,一个命令行程序,用于从 POCO 类生成 TypeScript 接口
  5. Fonlow.Poco2Ts,一个组件,用于从 POCO 类生成 TypeScript 接口

本文重点介绍生成 C# 客户端 API 库。有关 TypeScript 客户端 API 库,请参阅另一篇文章。如果您正在开发 .NET Core 应用程序,在阅读本文后,您可能还想查看“为 ASP.NET Core Web API 生成 C# 客户端 API”。

主要特点

  1. 生成的客户端 API 代码直接从 Web API 控制器方法、.NET 原始类型和 POCO 类映射而来。这类似于 WCF 中的 svcutil.exe 提供的功能。
  2. 控制器方法和 POCO 类的文档注释会复制到客户端代码中。

主要优点

  1. WebApiClientGen 与 ASP.NET Web API 无缝集成,只需很少的步骤/开销即可在 Web API 和客户端 API 之间进行设置、维护和同步,从而在 RAD 或敏捷软件开发中提高效率。
  2. 支持包括 decimal 在内的所有 .NET 原始类型。
  3. 支持 DataTimeDataTimeOffsetArrayTuple、动态对象、DictionaryKeyValuePair
  4. 强类型生成的代码会进行设计时类型检查和编译时类型检查。
  5. 提供高层抽象,使应用程序开发人员能够避免 RESTful 实践的琐碎技术细节。
  6. 丰富的元信息,包括文档注释,使 IDE 的智能感知更加有用,从而减少了应用程序开发人员阅读独立 API 文档的需求。

背景

如果您曾经使用 WCF 开发过基于 SOAP 的 Web 服务,您可能已经享受过SvcUtil.exe 生成的客户端 API 代码或 Visual Studio IDE 的服务引用。当迁移到 Web API 时,我觉得我回到了石器时代,因为在开发过程中我必须花费大量时间检查数据类型和函数原型,这消耗了我宝贵的脑力,而这些检查本应由计算机完成。

我在 2010 年基于 IHttpHandler/IHttpModule 开发了一些 RESTful Web 服务,这些服务不需要强类型数据,而是需要文档和流等任意数据。然而,我一直在开发更多需要高抽象和语义数据类型的 Web 项目。

我注意到 ASP.NET Web API 通过 ApiController 类支持高抽象和强类型函数原型,并且 ASP.NET MVC 框架可以选择性地提供漂亮生成的帮助页面来描述 API 函数。然而,在开发完 Web API 后,我必须手工编写一些非常基础且重复的客户端代码来消费 Web 服务。如果 Web API 是由他人开发的,我将不得不阅读在线帮助页面。

我怀念以前和 WCF 共度的美好时光。:) 客户端编程的开销应该减少。

因此,我一直在搜索和尝试寻找一些解决方案,以摆脱手工编写基础且重复的代码,从而可以将精力集中在客户端的业务逻辑构建上。以下是帮助开发客户端程序的开源项目列表

  1. WADL
  2. RAML with .NET
  3. WebApiProxy
  4. Swashbuckle 基于 Swagger
  5. AutoRest
  6. OData

虽然这些解决方案在一定程度上可以生成强类型客户端代码并减少重复性任务,但我发现没有一个能提供我所期望的流畅高效的编程体验。

  1. 强类型客户端数据模型映射到服务的数据模型
  2. 强类型函数原型映射到 ApiController 派生类的函数
  3. 批量代码生成方式,类似于 WCF 的方式,从而在 SDLC 中将开销降至最低
  4. 通过数据注释(使用 DataContractAttributeJsonObjectAttribute 等流行属性)来选择性地获取数据模型。
  5. 设计时和编译时进行类型检查
  6. 客户端数据模型、函数原型和文档注释的智能感知

这就是 WebApiClientGen

假设

  1. 您正在开发 ASP.NET Web API 2.x 应用程序,并将使用 C# 作为主要编程语言开发运行在 Windows 桌面、通用 Windows、Android 或 iOS 上的应用程序。
  2. 您和您的同事更倾向于通过服务器端和客户端的强类型函数来实现高抽象。
  3. POCO 类同时被 Web API 和 Entity Framework Code First 使用,您可能不想将所有数据类和类成员发布到客户端程序。

为了跟进这种新的客户端程序开发方式,最好有一个 ASP.NET Web API 项目,或者一个包含 Web API 的 MVC 项目。您可以使用现有项目,也可以创建一个演示项目。

Using the Code

步骤 0:在 Web MVC/API 项目中安装 NuGet 包 WebApiClientGen

安装过程还将把依赖的 NuGet 包 Fonlow.TypeScriptCodeDOMFonlow.Poco2Ts 添加到项目引用中。

此外,CodeGenController.cs(用于触发代码生成)已添加到项目的Controllers文件夹中。
CodeGenController 应该仅在开发期间的调试生成中可用,因为客户端 API 只应为 Web API 的每个版本生成一次。

#if DEBUG  //This controller is not needed in production release, 
           //since the client API should be generated during development of the Web API
using Fonlow.CodeDom.Web;
using System.Linq;
using System.Web.Http;

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
    {
        /// <summary>
        /// Trigger the API to generate WebApiClientAuto.cs 
        /// for an established client API project.
        /// </summary>
        /// <param name="settings"></param>
        /// <returns>OK if OK</returns>
        [HttpPost]
        public IHttpActionResult TriggerCodeGen(CodeGenSettings settings)
        {
            if (settings == null)
                return BadRequest("No settings");

            if (settings.ClientApiOutputs == null)
                return BadRequest("No settings/ClientApiOutputs");

            string webRootPath = System.Web.Hosting.HostingEnvironment.MapPath("~");
            Fonlow.Web.Meta.WebApiDescription[] apiDescriptions;
            try
            {
                apiDescriptions = 
                  Configuration.Services.GetApiExplorer().ApiDescriptions.Select
                  (d => Fonlow.Web.Meta.MetaTransform.GetWebApiDescription(d)).OrderBy
                  (d => d.ActionDescriptor.ActionName).ToArray();

            }
            catch (System.InvalidOperationException e)
            {
                System.Diagnostics.Trace.TraceWarning(e.Message);
                return InternalServerError(e);
            }

            if (!settings.ClientApiOutputs.CamelCase.HasValue)
            {
                var camelCase = GlobalConfiguration.Configuration.Formatters.
                    JsonFormatter.SerializerSettings.ContractResolver is 
                    Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver;
                settings.ClientApiOutputs.CamelCase = camelCase;
            }

            try
            {
                CodeGen.GenerateClientAPIs(webRootPath, settings, apiDescriptions);
            }
            catch (Fonlow.Web.Meta.CodeGenException e)
            {
                var s = e.Message + " : " + e.Description;
                System.Diagnostics.Trace.TraceError(s);
                return BadRequest(s);
            }

            return Ok("Done");
        }
    }

}
#endif

备注

  1. CodeGenController 安装在YourMvcOrWebApiProject/Controllers中,尽管 MVC 项目的脚手架有一个 API 文件夹用于 ApiController 的派生类。
  2. 然而,用于 .NET Core 的WebApiClientGenCore 不会安装 CodeGenController,您需要复制该文件

步骤 1:创建 .NET 客户端 API 项目

确保引用了以下程序包

  1. Microsoft ASP.NET Web API 2.2 客户端库
  2. Newtonsoft Json.NET。
  3. System.Runtime.Serialization
  4. System.ServiceModel
  5. System.ComponentModel.DataAnnotations

如下图所示

步骤 2:准备 JSON 配置数据

您的 Web API 项目可能具有如下 POCO 类和 API 函数

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; }

        /// <summary>
        /// Foreign key to Entity
        /// </summary>
        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<Address>();
        }

        [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<Address> 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<string> Lines;
    }

...
...

namespace DemoWebApi.Controllers
{
    [RoutePrefix("api/SuperDemo")]
    public class EntitiesController : ApiController
    {
        /// <summary>
        /// Get a person
        /// </summary>
        /// <param name="id">unique id of that guy</param>
        /// <returns>person in db</returns>
        [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 负载如下

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

        "DataModelAssemblyNames": [
            "DemoWebApi.DemoData",
            "DemoWebApi"
        ],

        "CherryPickingMethods": 3
    },

    "ClientApiOutputs": {
        "ClientLibraryProjectFolderName": "..\\DemoWebApi.ClientApi",
        "UseEnsureSuccessStatusCodeEx": true,
        "DataAnnotationsEnabled": true,
        "DataAnnotationsToComments": true,

        "Plugins": []
    }
}

建议将 JSON 负载保存到一个文件中,例如此文件

如果您在 Web API 项目中定义了所有 POCO 类,则应将 Web API 项目的程序集名称添加到“DataModelAssemblyNames”数组中。如果您出于良好分工的目的使用了一些专用的数据模型程序集,则应将相应的程序集名称添加到该数组中。您可以选择生成 TypeScript 客户端 API 代码,或 C# 客户端 API 代码,或两者都生成。

CodeGen 根据“CherryPickingMethods”(在下面的文档注释中描述)从 POCO 类生成 C# 客户端代理类。

/// <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(选择加入)。您可以使用任何方法或方法的组合。例如,3 表示 DataContractNewtonsoftJson。您甚至可以为不同的数据模型程序集定义不同的选择性提取方法。例如:

        "DataModels": [
            {
                "AssemblyName": "DemoWebApi.DemoData",
                "CherryPickingMethods": 3
            },

            {
                "AssemblyName": "DemoWebApi",
                "CherryPickingMethods": 1
            }
        ]

 

步骤 3:运行 Web API 项目的 DEBUG 版本

步骤 4:POST JSON 配置数据以触发客户端 API 代码的生成

在 IDE 中使用 IIS Express 运行 Web 项目。

然后,您可以使用 CurlPoster 或您喜欢的任何客户端工具将 content-type=application/json 的数据 **POST** 到 https://:10965/api/CodeGen。

提示

因此,基本上您只需一个步骤即可生成客户端 API,因为您不必每次都安装 NuGet 包。

编写一些批处理脚本来启动 Web API 并 POST JSON 配置数据应该不难。事实上,我已经为此起草了一个供您参考:一个 CreateClientApi.ps1,它在 IIS Express 上启动 Web (API) 项目然后 POST JSON 文件。因此,在大多数情况下,对于 Web API 和客户端 API 的持续更新/同步,您只需要执行步骤 3 并运行一个 Powershell 脚本。这大大减少了持续集成(CI)的开销。

此序列图说明了开发周期

发布客户端 API 库

完成这些步骤后,您现在将获得生成的 C# 客户端 API,类似于此示例

namespace DemoWebApi.DemoData.Client
{
    public class Address : object
    {
        
        public string City { get; set; }
        
        public string Country { get; set; } = "Australia";
               
        public System.Guid Id { get; set; }
        
        public string PostalCode { get; set; }
        
        public string State { get; set; }
        
        public string Street1 { get; set; }
        
        public string Street2 { get; set; }
        
        public DemoWebApi.DemoData.Client.AddressType Type { get; set; }
        
        /// <summary>
        /// It is a field
        /// </summary>
        public DemoWebApi.DemoData.Another.Client.MyPoint Location { get; set; }
    }
    
    public enum AddressType
    {
        
        Postal,
        
        Residential,
    }
    
    public class Company : DemoWebApi.DemoData.Client.Entity
    {
        
        /// <summary>
        /// BusinessNumber to be serialized as BusinessNum
        /// </summary>
        public string BusinessNumber { get; set; }
        
        public string BusinessNumberType { get; set; }
        
        public string[][] TextMatrix { get; set; }
        
        public int[,] Int2D { get; set; }
        
        public int[][] Int2DJagged { get; set; }
        
        public string[] Lines { get; set; }
    }
    
    public enum Days
    {
        
        Sat = 1,
        
        Sun = 2,
        
        Mon = 3,
        
        Tue = 4,
        
        Wed = 5,
        
        /// <summary>
        /// Thursday
        /// </summary>
        Thu = 6,
        
        Fri = 7,
    }
    
    /// <summary>
    /// Base class of company and person
    /// </summary>
    public class Entity : object
    {
        
        /// <summary>
        /// Multiple addresses
        /// </summary>
        public DemoWebApi.DemoData.Client.Address[] Addresses { get; set; }
        
        public System.Guid Id { get; set; }
        
        /// <summary>
        /// Name of the entity.
        /// Required
        /// </summary>
        [System.ComponentModel.DataAnnotations.RequiredAttribute()]
        public string Name { get; set; }
        
        public DemoWebApi.DemoData.Client.PhoneNumber[] PhoneNumbers { get; set; }
        
        public System.Uri Web { get; set; }
    }
    
    public class Person : DemoWebApi.DemoData.Client.Entity
    {
        
        /// <summary>
        /// Date of Birth.
        /// This is optional.
        /// </summary>
        public System.Nullable<System.DateTime> DOB { get; set; }
        
        public string GivenName { get; set; }
        
        public string Surname { get; set; }
    }
    
    public class PhoneNumber : object
    {
        
        public string FullNumber { get; set; }
        
        public System.Guid Id { get; set; }
        
        public DemoWebApi.DemoData.Client.PhoneType PhoneType { get; set; }
    }
    
    /// <summary>
    /// Phone type
    /// Tel, Mobile, Skyp and Fax
    /// 
    /// </summary>
    public enum PhoneType
    {
        
        /// <summary>
        /// Land line
        /// </summary>
        Tel,
        
        /// <summary>
        /// Mobile phone
        /// </summary>
        Mobile,
        
        Skype,
        
        Fax,
    }
}

namespace DemoWebApi.Controllers.Client
{
    using System;
    using System.Linq;
    using System.Collections.Generic;
    using System.Threading.Tasks;
    using System.Net.Http;
    using Newtonsoft.Json;
    using Fonlow.Net.Http;
    
    
    public partial class Entities
    {
        
        private System.Net.Http.HttpClient client;
        
        public Entities(System.Net.Http.HttpClient client)
        {
            if (client == null)
                throw new ArgumentNullException("Null HttpClient.", "client");

            if (client.BaseAddress == null)
                throw new ArgumentNullException("HttpClient has no BaseAddress", "client");

            this.client = client;
        }
        
        /// <summary>
        /// POST api/Entities/createPerson
        /// </summary>
        public async Task<long> CreatePersonAsync(DemoWebApi.DemoData.Client.Person p)
        {
            var requestUri = "api/Entities/createPerson";
            using (var requestWriter = new System.IO.StringWriter())
            {
            var requestSerializer = JsonSerializer.Create();
            requestSerializer.Serialize(requestWriter, p);
            var content = new StringContent(requestWriter.ToString(), System.Text.Encoding.UTF8, "application/json");
            var responseMessage = await client.PostAsync(requestUri, content);
            try
            {
                responseMessage.EnsureSuccessStatusCodeEx();
                var stream = await responseMessage.Content.ReadAsStreamAsync();
                using (JsonReader jsonReader = new JsonTextReader(new System.IO.StreamReader(stream)))
                {
                return System.Int64.Parse(jsonReader.ReadAsString());
                }
            }
            finally
            {
                responseMessage.Dispose();
            }
            }
        }
                    
        /// <summary>
        /// DELETE api/Entities/{id}
        /// </summary>
        public async Task DeleteAsync(long id)
        {
            var requestUri = "api/Entities/"+id;
            var responseMessage = await client.DeleteAsync(requestUri);
            try
            {
                responseMessage.EnsureSuccessStatusCodeEx();
            }
            finally
            {
                responseMessage.Dispose();
            }
        }
        
        /// <summary>
        /// GET api/Entities/Company/{id}
        /// </summary>
        public async Task<DemoWebApi.DemoData.Client.Company> GetCompanyAsync(long id)
        {
            var requestUri = "api/Entities/Company/"+id;
            var responseMessage = await client.GetAsync(requestUri);
            try
            {
                responseMessage.EnsureSuccessStatusCodeEx();
                var stream = await responseMessage.Content.ReadAsStreamAsync();
                using (JsonReader jsonReader = new JsonTextReader(new System.IO.StreamReader(stream)))
                {
                var serializer = new JsonSerializer();
                return serializer.Deserialize<DemoWebApi.DemoData.Client.Company>(jsonReader);
                }
            }
            finally
            {
                responseMessage.Dispose();
            }
        }
        

值得注意的是,服务类 Address 中的 EntityId 属性未出现在客户端数据模型中,因为显然您不希望将此类服务端的技�½�细节发布到客户端,客户端应用程序开发人员也不应关心此属性。

如果您希望外部开发人员使用您的 Web API,您可以发布 C# 客户端 API 代码或针对各种平台的已编译库。

使用生成的 API 代码

在使用 Visual Studio 等不错的文本编辑器编写客户端代码时,您可以获得良好的智能感知,因此您很少需要阅读 Web API 帮助页面。

支持通用 Windows 应用、Android 应用和 iOS 应用

对于通用 Windows 应用,您可以创建如下所示的客户端 API 库

对于 Android 应用,您可以使用 Mono.Android 创建如下所示的客户端 API 项目

对于 iOS 应用,您可以使用 Xamarin.iOS 创建如下所示的客户端 API 项目

提示

如果您希望使用相同的代码库为各种平台提供已编译的库,您可以创建对DemoWebApi.ClientApi文件夹中生成的WebApiClientAuto.cs文件的符号链接。

如图所示,点击“**添加为链接**”,您将在 DemoWebApi.iOSClientApi 项目中创建一个对 cs 文件的符号链接。或者,您可以使用共享项目,或者更推荐使用.NET Standard项目。

优势总结

  1. 与 ASP.NET Web API 无缝集成,只需很少的步骤/开销即可在 Web API 和客户端 API 之间进行设置、维护和同步。
  2. 支持所有内置类型,包括 decimal。
  3. 支持 DataTimeDataTimeOffsetArrayTuple、动态对象、DictionaryKeyValuePair
  4. 强类型生成的代码将经过设计时类型检查和编译时类型检查。
  5. 高抽象。
  6. 智能感知。

SDLC。

因此,基本上,您编写 Web API 代码(包括 API 控制器和数据模型),然后执行CreateClientApi.ps1。就这样。WebApiClientGenCreateClientApi.ps1 将为您处理其余的事情。

团队协作

本节介绍了一些基本的团队协作场景。不同公司和团队的情况和背景可能不同,因此您应相应地调整您的团队实践。

您的团队有一位后端开发人员 Brenda 负责 Web API,一位前端开发人员 Frank 负责前端。每个开发机器都已正确设置了集成测试环境,因此大部分 CI 工作可以在每台开发机器上完成,而无需团队 CI 服务器。主干开发是默认的分支实践。

1 个包含后端代码和前端代码的存储库

  1. Brenda 编写了一些新的 Web API 代码并进行了构建。
  2. Brenda 执行 CreateClientApi.ps1 来生成客户端代码。
  3. Brenda 编写并运行了一些基本的集成测试用例来测试 Web API。
  4. Brenda 将更改提交/推送到主开发分支或主干。
  5. Frank 更新/拉取更改,进行构建,并运行测试用例。
  6. Frank 基于新的 Web API 和客户端 API 开发新的前端功能。

1 个后端存储库和 1 个前端存储库

Brenda 调整了 CodeGen.json,该文件会将生成的代码定向到前端存储库工作目录中的客户端 API 文件夹。

  1. Brenda 编写了一些新的 Web API 代码并进行了构建。
  2. Brenda 执行 CreateClientApi.ps1 来生成客户端代码。
  3. Brenda 编写并运行了一些基本的集成测试用例来测试 Web API。
  4. Brenda 将更改提交/推送到两个存储库的主开发分支或主干。
  5. Frank 使用两个存储库更新/拉取更改,进行构建,并运行测试用例。
  6. Frank 基于新的 Web API 和客户端 API 开发新的前端功能。

兴趣点

虽然 ASP.NET MVC 和 Web API 使用 NewtonSoft.Json 进行 JSON 应用,但 NewtonSoft.Json 可以很好地处理由 DataContractAttribute 装饰的 POCO 类

CLR 命名空间通过添加“Client”作为后缀来转换为客户端命名空间。例如,命名空间 My.Name.space 将被转换为 My.Name.space.Client

从某种角度来看,服务命名空间/函数名与客户端命名空间/函数名之间的一对一映射暴露了服务的实现细节,这通常不被推荐。然而,传统的 RESTful 客户端编程要求程序员了解服务函数的 URL 查询模板,而查询模板是服务的实现细节。因此,这两种方法在某种程度上都暴露了服务的实现细节。传统 RESTful 客户端编程需要了解 URL 查询模板,而 WebApiClientGen 则将客户端函数命名为与服务函数相同,这会暴露实现细节。所以,这两种方法都会在某种程度上暴露服务实现细节。

对于客户端应用程序开发人员,经典的函数原型如

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 将文档注释复制到生成的 C# 代码中,因此您很少需要阅读 MVC 生成的帮助页面,并且使用该服务进行客户端编程将变得更加无缝。

提示

并且,编写脚本来完全自动化一些步骤以实现持续集成不应该很难。

 

WebApiClientGen vs Swagger

虽然 Swagger 工具链主要用于元数据优先的方法,但也有一些工具支持代码优先的方法,即服务器端工具生成 Swagger 定义文件,客户端工具根据定义生成代码。如果您使用 Swashbuckle 和 NSwag,本节将为您提供简要的比较。这里的 Swagger 指的是 ASP.NET Core Web API 上的 Swashbuckle.AspNetCore,以及用于生成客户端代码的 NSwagStudio。

C# 客户端

顾名思义,强类型客户端 API 生成器 提供服务器和 C# 客户端之间尽可能精确的数据类型映射。

Swagger 不支持

  1. 用户定义的结构体。
  2. 对象
  3. dynamic

Swagger 为以下类型提供数据类型映射

  1. Decimal ==> double
  2. Nullable<T> ==> T
  3. float ==>double
  4. uint, short, byte ==> int
  5. ulong ==> long
  6. char==> string
  7. Tuple ==> 生成具有与 Tuple 类似结构的已定义类型
  8. int[,] ==> ICollection<object>
  9. int[][] ==> ICollection<int>
  10. KeyValuePair ==> 生成具有与 KeyValuePair 类似结构的已定义类型

Swagger 生成冗长、庞大且复杂的代码

在 sln 中,Core3WebApi 使用 WebApiClientGen,SwaggerDemo 使用 Swashbuckle.AspNetCore 来创建 Open API 定义。仅生成异步函数时,WebApiClientGen 生成的代码为 97KB,调试生成为 166KB,发布生成为 117KB,而 Swagger 的 NSwagStudio 生成的代码为 489KB-495KB,调试生成为 340KB-343KB,发布生成为 263KB-283KB。

Swagger 生成冗长的 GeneratedCodeAttribute

根据 https://docs.microsoft.com/en-us/dotnet/api/system.codedom.compiler.generatedcodeattribute?view=netcore-3.1,GeneratedCodeAttribute 类可用于代码分析工具来识别计算机生成的代码,并根据生成代码的工具和版本提供分析。

将生成的代码放入专用的程序集中,仅包含生成的代码,这是一个好习惯。因此,应用程序程序员可以简单地将此程序集排除在代码分析工具之外。因此,生成的代码中不需要 GeneratedCodeAttribute。

TypeScript 客户端

WebApiClientGen

  • jQuery with callbacks
  • Angular 2+
  • Axios
  • Aurelia
  • Fetch API

 

NSwag

  • JQuery with Callbacks, JQueryCallbacks
  • JQuery with promises JQueryPromises
  • AngularJS using $http, AngularJS
  • Angular (v2+) using the http service, Angular
  • window.fetch API and ES6 promises, Fetch (use this template in your React/Redux app)
  • Aurelia using the HttpClient from aurelia-fetch-client, Aurelia (based on the Fetch template)
  • Axios (preview)

 

WebApiClientGen 相对于 Swagger 的优势?

对于生成 C# 客户端,WebApiClientGen 支持更多的 .NET 内置数据类型,并提供更精确的数据类型映射。精确的类型映射使客户端编程更容易实现高质量,因为由于正确的类型约束,集成测试很容易捕获超出范围的数据。

更小的代码和更小的编译后的镜像总是受欢迎的。

生成客户端代码的手动步骤更少,速度更快。

Swagger 相对于 WebApiClientGen 的优势?

这里的 Swagger 指的是 Open API 标准和相应的工具链。

Swagger 是一种开放标准且平台中立的标准,得到主流软件供应商的支持,并由全球数百名开发人员开发。Microsoft Docs 在 https://docs.microsoft.com/en-us/aspnet/core/tutorials/web-api-help-pages-using-swagger?view=aspnetcore-3.1 设有专门的 Swagger 部分,Microsoft 一直将其用于自己的 Web API 产品。

Swagger 支持对 HTTP 标头进行细粒度控制,而 WebApiClientGen 则(按设计)忽略了此区域。

在线帮助如何?

Swashbuckle.AspNetCore 提供“描述 Web API 功能的丰富、可定制的体验”。

WebApiClientGen 将已发布数据类型和控制器的源内文档复制到客户端代码中,像 Visual Studio 这样的优秀 IDE 可以在客户端代码中显示带有源内文档的智能感知。这最大限度地减少了对在线帮助的需求。

如果您确实喜欢在线帮助,可以为 C# 客户端代码使用 Sandcastle,为 Angular 2+ 客户端代码使用Compodoc,并为基于其他 JavaScript 框架的 TypeScript 客户端代码使用 TypeDoc

WebApiClientGen 和 Swagger 能共存吗?

这里的 Swagger 指的是 ASP.NET Core Web API 上的 Swashbuckle.AspNetCore,以及用于生成客户端代码的 NSwagStudio。

答案是可以。

这两个产品在 .NET 领域有很大的重叠,而 Swagger 涵盖了更广泛、更深入的领域。

如果您正在开发 ASP.NET (Core) Web API 并期望所有客户端都仅用 C# 和 TypeScript 编写,那么 WebApiClientGen 将为您带来更多优势。

当您需要支持 C# 和 TypeScript 以外语言编写的客户端时,可以考虑将 Swagger 引入您的 Web API,并生成 JSON 或 YAML 格式的 Open API 定义文件。

通过 WebApiClientGen 和 Swagger 实现完美的 SDLC

每当您作为后端开发人员更新 Web API 时,您可以运行 WebApiClientGen 和批处理文件,为其他客户端开发人员生成 C# 客户端代码和 TypeScript 客户端代码。Web API 的 Swagger 端点提供 Open API 定义文件,因此使用其他语言的客户端开发人员可以生成其他语言的客户端 API 代码。

这样您就可以同时获得 WebApiClientGen 和 Swagger 的最佳优势。

更新:为了做得更好,我已经制作了强类型 Open API 客户端生成器,它根据 Open API / Swagger 定义生成 C# 和 TypeScript 客户端代码。生成的代码更简单,并涵盖更多功能性数据类型。在某些内容和上下文中,它可能是 NSwag 的一个不错的替代方案。

参考文献

© . All rights reserved.