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

目录

介绍

主要特征

主要好处

背景

推定(Presumptions)

使用代码

步骤0:将NuGet软件包WebApiClientGen安装到Web MVC/API项目

步骤1:建立.NET Client API项目

步骤2:准备JSON配置数据

步骤3:运行Web API项目的DEBUG构建

步骤4:发布JSON Config数据以触发客户端API代码的生成

发布客户端API库

使用生成的API代码

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

优点总结

SDLC

团队合作

兴趣点

Swashbuckle +AutoRest VS. WebApiClientGen


介绍

用于开发ASP.NET Web APIASP.NET Core Web API的客户端程序,  强类型客户端API生成器以C#代码和TypeScript代码生成强类型客户端API,以最大程度地减少重复性任务并提高应用程序开发人员的生产率和产品质量。

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

  1. C#中用于强类型客户端API的代码生成器,支持桌面,Universal WindowsAndroidiOS
  2. 用于jQueryAngular 2+AureliaTypeScript以及使用Axios TypeScript / JavaScript应用程序中的强类型客户端API的代码生成器
  3. TypeScript CodeDOM,一种TypeScriptCodeDOM组件,从.NET FrameworkCodeDOM派生而来。
  4. POCO2TS.exe,这是一个从POCO类生成TypsScript接口的命令行程序。
  5. Fonlow.Poco2Ts,从POCO类生成TypsScript接口的组件。

主要特征

  1. 生成的客户端API代码直接从Web API控制器方法,.NET基本类型和POCO类进行映射。这类似于WCF中提供的svcutil.exe
  2. 控制器方法和POCO类的文档注释将复制到客户端代码。

主要好处

  1. WebApiClientGen RAD或敏捷软件开发期间,只需很少的步骤/开销就可以与ASP.NET Web API无缝集成,以在Web API和客户端API之间进行设置、维护和同步。
  2. 支持所有.NET基本类型,包括十进制。
  3. 支持DataTimeDataTimeOffsetArrayTuple,动态对象,DictionaryKeyValuePair
  4. 强类型生成的代码需要进行设计时类型检查和编译时类型检查。
  5. 提供高级抽象,使应用程序开发人员免受传统HTTP客户端调用带来的RESTful实践琐碎技术细节的影响。
  6. 丰富的元信息(包括文档注释)使IDE intellisense更加有用,因此应用程序开发人员较少需要阅读单独的API文档。

背景

如果您曾经使用WCF开发过基于SOAPWeb服务,则可能会喜欢使用SvcUtil.exeVisual Studio IDE的服务引用生成的客户端API代码。转向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 based on Swagger
  5. AutoRest
  6. OData

虽然这些解决方案可以生成强类型的客户端代码并在某种程度上减少重复的任务,但我发现它们都无法给我带来我所期望的所有流畅而高效的编程经验:

  1. 映射到服务数据模型的强类型客户端数据模型。
  2. 强类型函数原型映射到的派生类ApiController的功能。
  3. WCF编程那样以批发方式生成代,因此SDLC期间的开销最少。
  4. 使用流行的属性(如DataContractAttributeJsonObjectAttribute等)通过数据注释来挑选数据模型。
  5. 在设计时和编译时进行类型检查。
  6. 用于客户端数据模型,功能原型和文档注释的智能感知。

这是WebApiClientGen

推定(Presumptions)

  1. 您将开发ASP.NET Web API 2.x应用程序,并将使用C#作为主要编程语言来开发在Windows桌面、Universal WindowsAndroidiOS上运行的应用程序。
  2. 您和开发人员都喜欢通过服务器端和客户端中的强类型函数来实现高度抽象。
  3. Web APIEntity Framework Code First都使用POCO类,并且您可能不想将所有数据类和成员发布到客户端程序。

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

使用代码

步骤0:将NuGet软件包WebApiClientGen安装到Web MVC/API项目

安装还将安装相关的NuGet软件包Fonlow.TypeScriptCodeDOMFonlow.Poco2Ts到项目引用。

此外,用于触发CodeGenCodeGenController.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项目的脚手架上具有ApiController派生类的文件夹API
  2. WebApiClientGenCore没有安装CodeGenController,您应该将文件复制过来

步骤1:建立.NET Client 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",
        "GenerateBothAsyncAndSync": true,

        "Plugins": []
    }
}

有效负载示例随v1.8.0一起提供,并且可以在此处找到早期版本。

建议的JSON有效载荷保存到像一个样的文件中。

如果在Web API项目中定义了所有POCO类,则应将Web API项目的程序集名称放在DataModelAssemblyNames数组中。如果您有一些专用的数据模型程序集可以很好地分离关注点,则应将相应的程序集名称放入数组中。您可以选择生成TypeScript客户端API代码或C#客户端API代码,或同时生成两者。

CodeGen根据CherryPickingMethodsPOCO类生成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:运行Web API项目的DEBUG构建

步骤4:发布JSON Config数据以触发客户端API代码的生成

IIS Express上的IDE中运行Web项目。

然后,您可以使用CurlPoster或任何您喜欢的客户端工具通过使用content-type=application/json POSThttp://localhost:10965/api/CodeGen 

提示

因此,基本上,您只需要一步就可以生成客户端API,因为您不需要每次都安装NuGet软件包。

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

此序列图说明了开发周期:

发布客户端API

完成这些步骤之后,现在您已经生成了C#客户端API,类似于以下示例:

namespace DemoWebApi.DemoData.Client
{    
    public enum AddressType
    {        
        Postal,        
        Residential,
    }
    
    public enum Days
    {        
        Sat = 1,        
        Sun = 2,        
        Mon = 3,        
        Tue = 4,        
        Wed = 5,        
        Thu = 6,        
        Fri = 7,
    }
    
    public class Address : object
    {        
        private System.Guid _Id;        
        private string _Street1;        
        private string _Street2;        
        private string _City;        
        private string _State;        
        private string _PostalCode;        
        private string _Country;        
        private DemoWebApi.DemoData.Client.AddressType _Type;        
        private DemoWebApi.DemoData.Another.Client.MyPoint _Location;        
        public System.Guid Id
        {
            get
            {
                return _Id;
            }
            set
            {
                _Id = value;
            }
        }
        
        public string Street1
        {
            get
            {
                return _Street1;
            }
            set
            {
                _Street1 = value;
            }
        }
        
        public string Street2
        {
            get
            {
                return _Street2;
            }
            set
            {
                _Street2 = value;
            }
        }
        
        public string City
        {
            get
            {
                return _City;
            }
            set
            {
                _City = value;
            }
        }
        
        public string State
        {
            get
            {
                return _State;
            }
            set
            {
                _State = value;
            }
        }
        
        public string PostalCode
        {
            get
            {
                return _PostalCode;
            }
            set
            {
                _PostalCode = value;
            }
        }
        
        public string Country
        {
            get
            {
                return _Country;
            }
            set
            {
                _Country = value;
            }
        }
        
        public DemoWebApi.DemoData.Client.AddressType Type
        {
            get
            {
                return _Type;
            }
            set
            {
                _Type = value;
            }
        }
        
        public DemoWebApi.DemoData.Another.Client.MyPoint Location
        {
            get
            {
                return _Location;
            }
            set
            {
                _Location = value;
            }
        }
    }
    
    public class Entity : object
    {
        
        private System.Guid _Id;
        
        private string _Name;
        
        private DemoWebApi.DemoData.Client.Address[] _Addresses;
        
        public System.Guid Id
        {
            get
            {
                return _Id;
            }
            set
            {
                _Id = value;
            }
        }
        
        [System.ComponentModel.DataAnnotations.RequiredAttribute()]
        public string Name
        {
            get
            {
                return _Name;
            }
            set
            {
                _Name = value;
            }
        }
        
        public DemoWebApi.DemoData.Client.Address[] Addresses
        {
            get
            {
                return _Addresses;
            }
            set
            {
                _Addresses = value;
            }
        }
    }
    
    public class Person : DemoWebApi.DemoData.Client.Entity
    {
        
        private string _Surname;
        
        private string _GivenName;
        
        private System.Nullable<System.DateTime> _BirthDate;
        
        public string Surname
        {
            get
            {
                return _Surname;
            }
            set
            {
                _Surname = value;
            }
        }
        
        public string GivenName
        {
            get
            {
                return _GivenName;
            }
            set
            {
                _GivenName = value;
            }
        }
        
        public System.Nullable<System.DateTime> BirthDate
        {
            get
            {
                return _BirthDate;
            }
            set
            {
                _BirthDate = value;
            }
        }
    }
    
    public class Company : DemoWebApi.DemoData.Client.Entity
    {
        
        private string _BusinessNumber;
        
        private string _BusinessNumberType;
        
        private string[][] _TextMatrix;
        
        private int[][] _Int2DJagged;
        
        private int[,] _Int2D;
        
        private string[] _Lines;
        
        public string BusinessNumber
        {
            get
            {
                return _BusinessNumber;
            }
            set
            {
                _BusinessNumber = value;
            }
        }
        
        public string BusinessNumberType
        {
            get
            {
                return _BusinessNumberType;
            }
            set
            {
                _BusinessNumberType = value;
            }
        }
        
        public string[][] TextMatrix
        {
            get
            {
                return _TextMatrix;
            }
            set
            {
                _TextMatrix = value;
            }
        }
        
        public int[][] Int2DJagged
        {
            get
            {
                return _Int2DJagged;
            }
            set
            {
                _Int2DJagged = value;
            }
        }
        
        public int[,] Int2D
        {
            get
            {
                return _Int2D;
            }
            set
            {
                _Int2D = value;
            }
        }
        
        public string[] Lines
        {
            get
            {
                return _Lines;
            }
            set
            {
                _Lines = value;
            }
        }
    }
    
    public class MyPeopleDic : object
    {
        
        private System.Collections.Generic.Dictionary
                <string, DemoWebApi.DemoData.Client.Person> _Dic;
        
        private System.Collections.Generic.Dictionary<string, string> _AnotherDic;
        
        private System.Collections.Generic.Dictionary<int, string> _IntDic;
        
        public System.Collections.Generic.Dictionary
               <string, DemoWebApi.DemoData.Client.Person> Dic
        {
            get
            {
                return _Dic;
            }
            set
            {
                _Dic = value;
            }
        }
        
        public System.Collections.Generic.Dictionary<string, string> AnotherDic
        {
            get
            {
                return _AnotherDic;
            }
            set
            {
                _AnotherDic = value;
            }
        }
        
        public System.Collections.Generic.Dictionary<int, string> IntDic
        {
            get
            {
                return _IntDic;
            }
            set
            {
                _IntDic = value;
            }
        }
    }
}
namespace DemoWebApi.DemoData.Another.Client
{    
    public struct MyPoint
    {
        
        public double X;
        
        public double Y;
    }
}

    public partial class Entities
    {
        
        private System.Net.Http.HttpClient client;
        
        private System.Uri baseUri;
        
        public Entities(System.Net.Http.HttpClient client, System.Uri baseUri)
        {
            if (client == null)
                throw new ArgumentNullException("client", "Null HttpClient.");

            if (baseUri == null)
                throw new ArgumentNullException("baseUri", "Null baseUri");

            this.client = client;
            this.baseUri = baseUri;
        }
        
        /// <summary>
        ///
        /// PUT api/SuperDemo/link?id={id}&relationship={relationship}
        /// </summary>
        public async Task<bool> LinkPersonAsync
             (long id, string relationship, DemoWebApi.DemoData.Client.Person person)
        {
            var requestUri = this.baseUri + 
                "api/SuperDemo/link?id="+id+"&relationship="+relationship;
            using (var requestWriter = new System.IO.StringWriter())
            {
            var requestSerializer = JsonSerializer.Create();
            requestSerializer.Serialize(requestWriter, person);
            var content = new StringContent(requestWriter.ToString(), 
                          System.Text.Encoding.UTF8, "application/json");
            var responseMessage = await client.PutAsync(requestUri, content);
            responseMessage.EnsureSuccessStatusCode();
            var stream = await responseMessage.Content.ReadAsStreamAsync();
            using (JsonReader jsonReader = 
                  new JsonTextReader(new System.IO.StreamReader(stream)))
            {
            var serializer = new JsonSerializer();
            return System.Boolean.Parse(jsonReader.ReadAsString());
            }
            }
        }
        
        /// <summary>
        ///
        /// PUT api/SuperDemo/link?id={id}&relationship={relationship}
        /// </summary>
        public bool LinkPerson(long id, string relationship, 
                               DemoWebApi.DemoData.Client.Person person)
        {
            var requestUri = this.baseUri + "api/SuperDemo/link?id="+id+
                                            "&relationship="+relationship;
            using (var requestWriter = new System.IO.StringWriter())
            {
            var requestSerializer = JsonSerializer.Create();
            requestSerializer.Serialize(requestWriter, person);
            var content = new StringContent(requestWriter.ToString(), 
                          System.Text.Encoding.UTF8, "application/json");
            var responseMessage = this.client.PutAsync(requestUri, content).Result;
            responseMessage.EnsureSuccessStatusCode();
            var stream = responseMessage.Content.ReadAsStreamAsync().Result;
            using (JsonReader jsonReader = 
                   new JsonTextReader(new System.IO.StreamReader(stream)))
            {
            var serializer = new JsonSerializer();
            return System.Boolean.Parse(jsonReader.ReadAsString());
            }
            }
        }
        
        /// <summary>
        ///
        /// GET api/SuperDemo/Company?id={id}
        /// </summary>
        public async Task<DemoWebApi.DemoData.Client.Company> GetCompanyAsync(long id)
        {
            var requestUri = this.baseUri + "api/SuperDemo/Company?id="+id;
            var responseMessage = await client.GetAsync(Uri.EscapeUriString(requestUri));
            responseMessage.EnsureSuccessStatusCode();
            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);
            }
        }
        
        /// <summary>
        ///
        /// GET api/SuperDemo/Company?id={id}
        /// </summary>
        public DemoWebApi.DemoData.Client.Company GetCompany(long id)
        {
            var requestUri = this.baseUri + "api/SuperDemo/Company?id="+id;
            var responseMessage = this.client.GetAsync(Uri.EscapeUriString(requestUri)).Result;
            responseMessage.EnsureSuccessStatusCode();
            var stream = responseMessage.Content.ReadAsStreamAsync().Result;
            using (JsonReader jsonReader = 
                   new JsonTextReader(new System.IO.StreamReader(stream)))
            {
            var serializer = new JsonSerializer();
            return serializer.Deserialize<DemoWebApi.DemoData.Client.Company>(jsonReader);
            }
        }

如果希望某些外部开发人员使用您的Web API,则可以发布针对各种平台的C#客户端API代码或编译的库,以及由ASP.NET MVC框架生成的帮助页面。

使用生成的API代码

这是一个简单的例子:

var httpclient = new system.net.http.httpclient();
var api = new demowebapi.controllers.client.entities(httpclient, baseuri);
person person = new person()
{
    name = "some one",
    surname = "one",
    givenname = "some",
    birthdate = datetime.now.addyears(-20),
    addresses = new address[]{new address(){
        city="brisbane",
        state="qld",
        street1="somewhere",
        street2="over the rainbow",
        postalcode="4000",
        country="australia",
        type= addresstype.postal,
        location = new demowebapi.demodata.another.client.mypoint() {x=4, y=9 },
    }},
};

var id = api.createperson(person);

在像Visual Studio这样的不错的文本编辑器中编写客户端代码时,您可能会得到很好的智能提示,因此您几乎不需要阅读Web API帮助页面。

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

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

对于Android应用程序,您可能具有这样的客户端API项目,如Mono.Android

对于iOS应用,您可以使用以下命令创建一个客户端API项目,如Xamarin.iOS

提示

如果你想用相同的代码库为各种平台提供编译库,您可以创建一个符号链接到文件WebApiClientAuto.cs,其在文件夹DemoWebApi.ClientApi中生成。

如屏幕快照所示,单击添加为链接 ”,您将在项目DemoWebApi.iOSClientApi中创建一个指向CS文件的符号链接。或者,您可以使用Shared Project,或更优选地使用.NET Standard项目。

优点总结

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

SDLC

因此,基本上,您可以制作包括API控制器和数据模型在内的Web API代码,然后执行CreateClientApi.ps1。就是这样。WebApiClientGenCreateClientApi.ps1将为您完成其余工作。

团队合作

本节描述团队合作的一些基本方案。在不同的公司和团队中,情况和上下文可能有所不同,因此您应相应地调整团队实践。

您的团队有一个在Web API上工作的后端开发人员Brenda,以及在前端上工作的前端开发人员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 MVCWeb APINewtonSoft.Json用于JSON应用程序,但NewtonSoft.Json可以很好地处理由DataContractAttribute装饰的POCO

通过添加后缀ClientCLR名称空间转换为客户端名称空间。例如,名称空间My.Name.space将转换为My.Name.space.Client

从某种角度来看,服务名称空间/函数名称与客户端名称空间/函数名称之间的一对一映射公开了服务的实现细节,通常不建议这样做。但是,传统的RESTful客户端编程要求程序员注意服务函数的URL查询模板,并且查询模板包含服务的实现细节。因此,这两种方法都在某种程度上(一种或另一种)公开了服务的实现细节。

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

ReturnType DoSomething(Type1 t1, Type2 t2 ...)

API函数,其余是传输的技术实现细节:TCP / IPHTTPSOAP,面向资源,基于CRUDURIRESTfulXMLJSON等。函数原型和一段API文档应该足以调用API函数。至少在操作成功时,客户端应用程序开发人员不必关心传输的那些实现细节。仅当出现错误时,开发人员才需要关心处理错误的技术细节。例如,在基于SOAPWeb服务中,您必须了解SOAP错误。在RESTful Web服务中,您可能必须处理HTTP状态代码。

而且查询模板几乎没有提供API函数的语义含义。相比之下,WebApiClientGen以服务函数命名客户端函数,就像默认情况下WCF中的SvcUtil.exe一样,因此只要服务开发人员以良好的语义名称命名服务函数,生成的客户端函数就具有良好的语义。

在同时涵盖服务开发和客户端开发的SDLC全景图中,服务开发人员具有服务函数的语义含义,通常在函数描述之后命名函数是一种良好的编程习惯。面向资源的CRUD可能具有语义含义,或者仅仅是函数描述的技术翻译。

WebApiClientGen 将文档注释复制到生成的C#代码中,因此您几乎不需要阅读由MVC生成的帮助页面,并且使用该服务的客户端编程将变得更加无缝。

提示

对于持续集成,编写脚本以完全自动化某些步骤并不难。

Swashbuckle +AutoRest VS. WebApiClientGen

Swashbukle通过阅读Web APIApiExplorer生成Swagger(meta),因此不同平台的客户端程序员可以使用相应的语言生成客户端API

AutoRest读取Swagger元数据并使用C#和JavaScript生成客户端API,并且可以肯定的是,只要服务可以提供Swagger元数据,它就可以为以任何编程语言编码的RESTful Web服务生成客户端API

因此,Swashbukle + AutoRest 几乎可以提供WebApiClientGen所提供的服务。

WebApiClientGen通过阅读ApiExplorer生成C#和TypeScript中的客户端API代码,但不涉及Swagger(meta)。它使用起来更简单、更有效,并且涵盖了更多的数据类型,例如用于货币计算的.NET十进制类型和Tuple类型等。并且Swagger不支持泛型、Tuple.NET的十进制类型。

在较高级别的,Swagger适用于模型优先方法,而WebApiClient适用于代码优先方法。Swashbuckle + AutoRest尝试支持代码优先方法,但要进行一些额外的操作。

WebApiClientGen不能完全取代SwashbukleAutoRest。如果您正在开发Web API,并且您将以C#和为您自己、您的团队或外部团队生成的TypeScript格式提供客户端API库,那么WebApiClientGen将更加无缝、直接和全面,但是SDLC的开销更少。

如果您需要支持PHPJavaJavaScriptC#和C ++等客户端API,并针对Swagger的不足来调整Web API设计,那么您可能会发现Swagger及其附件可能更可行。

简而言之,Swagger工具链支持更广泛的格局,因此在SDLC期间具有更多开销,而支持的数据类型较少,而WebApiClientGen针对ASP.NET Web API进行了优化,因此在SDLC期间具有更少的开销,并且支持的数据类型也更多。

发布了70 篇原创文章 · 获赞 130 · 访问量 42万+

猜你喜欢

转载自blog.csdn.net/mzl87/article/details/103748233