Создание клиентского API-интерфейса TypeScript для Web API
Создание строго типизированного клиентского API в TypeScript для ASP.NET Web API
Строго типизированные клиентские API-генераторы генерируют строго типизированный клиентский API в кодах C # и кодах TypeScript для минимизации повторяющихся задач и повышения производительности и качества при разработке клиентских программ. Затем вы можете предоставить или опубликовать либо сгенерированные исходные коды, либо скомпилированные клиентские API-интерфейсы для себя и других разработчиков в вашей команде или партнерах B2B.
Этот проект предусматривает работу со следующими продуктами:
- Генератор кода для строго типизированного клиентского API на C #, поддерживающего рабочий стол, Universal Windows, Android и iOS.
- Генератор кода для строго типизированного клиентского API в TypeScript для jQuery и Angular 2.
- TypeScript CodeDOM - компонент CodeDOM для TypeScript, полученный из CodeDOM .NET Framework.
- POCO2TS.exe - программа командной строки, которая генерирует интерфейсы TypsScript из классов POCO.
- Fonlow.Poco2Ts - компонент, который генерирует интерфейсы TypsScript из классов POCO
Эта статья посвящена генерации API-интерфейса API-интерфейса для JavaScript для jQuery.
Основные характеристики
- Созданные коды API-клиентов напрямую сопоставляются с методами контроллера веб-API, примитивными типами .NET и классами POCO. Это похоже на то, что предложил svcutil.exe в WCF.
- Комментарии Doc о методах контроллера и классах POCO копируются в клиентские коды.
Основные преимущества
- WebApiClientGen легко интегрируется с ASP.NET Web API с небольшим числом этапов и небольшими накладными расходами для настройки, поддержки и синхронизации между API Web API и клиентскими API, во время RAD или Agile Software Development.
- Поддержка всех примитивных типов .NET, включая десятичные.
- Поддержка DataTime, DataTimeOffset, Array, Tuple, Dynamic Object, Dictionary и KeyValuePair
- Строго типизированные сгенерированные коды подлежат проверке типа времени разработки и проверке типа времени компиляции.
- Обеспечение высокого уровня абстракции, ограждение разработчиков приложений от тривиальных технических подробностей практики RESTful с традиционными вызовами HTTP-клиента
- Богатая метаинформация, включая комментарии doc, делает IDE intellisense более полезной, поэтому разработчикам приложений не придется много читать отдельных API-документов.
SDLC
Таким образом, вы создаете Web API коды, включая API-контроллеры и модели данных, а затем выполняете CreateClientApi.ps1. Вот и все. WebApiClientGen и CreateClientApi.ps1 сделают для вас все остальное.
Бэкграунд
Если вы когда-либо разрабатывали базовые веб-службы SOAP с использованием WCF, вам, возможно, понравилось использование кодов API-клиентов, сгенерированных SvcUtil.exe или ссылок на веб-службу среды Visual Studio IDE. Когда я перешел на веб-API, я почувствовал, что вернулся к каменному веку, так как мне приходилось много проверять тип данных во время разработки, затрачивая много умственных усилий, в то время как выполнять эту работу должны были вычисления .
Я разработал некоторые веб-службы RESTful поверх IHttpHandler / IHttpModule в 2010 году для некоторых веб-служб, которые обрабатывали не строго типизированные данные, а произвольные данные, такие как документы и потоки. Тем не менее, я получаю больше веб-проектов со сложной бизнес-логикой и типами данных, и я хотел бы использовать высоко абстрагируемые и семантические типы данных в SDLC.
Я вижу, что ASP.NET Web API поддерживает высоко абстрагированные и строго типизированные прототипы функций через класс ApiController, а структура ASP.NET MVC опционально предоставляет хорошо сгенерированную страницу справки, описывающую функции API. Однако после разработки веб-API мне пришлось обрабатывать некоторые примитивные и повторяющиеся клиентские коды для использования веб-сервисов. Когда веб-API был разработан другими пользователями, мне приходилось читать интерактивные страницы справки, а уже затем создавать.
Поэтому я искал и пытался найти некоторые решения, которые могли бы освободить меня от создания примитивных и повторяющихся кодов, чтобы я мог сосредоточиться на построении бизнес-логики на стороне клиента на более высоких технических абстракциях. Вот список проектов с открытым исходным кодом, которые помогают разрабатывать клиентские программы:
Несмотря на то, что эти решения могли генерировать строго типизированные клиентские коды и сокращать в некоторой степени повторяющиеся задачи, я обнаружил, что ни одно из них не способно дать мне весь эффективный опыт программирования, который я ожидал:
- Отображение строго типизированных моделей клиентских данных для моделей данных службы.
- Отображение строго типизированных прототипов функций для функций производных классов ApiController.
- Генерация кода в оптовом стиле, как способ программирования WCF.
- Модели данных отбора данных посредством аннотаций данных с использованием популярных атрибутов, таких как DataContractAttribute и JsonObjectAttribute и т. д.
- Проверка типов во время разработки и во время компиляции.
- Ввод с автодополнением для моделей данных клиентов, прототипов функций и комментариев к документам.
И здесь приходит WebApiClientGen.
Предположения
- Вы разрабатываете приложения ASP.NET Web API 2.x и планируете разрабатывать библиотеки JavaScript для веб-интерфейса на основе AJAX с помощью jQuery или SPA с Angular2.
- Вы и другие разработчики предпочитаете высокую абстракцию с помощью строго типизированных функций как на стороне сервера, так и на стороне клиента, и используете TypeScript.
- Классы POCO используются как Web API, так и Entity Framework Code First, и вы не можете публиковать все классы данных и их члены в клиентских программах.
И, возможно, лучше, если вы или ваша команда одобряете разработку на основе Trunk, поскольку дизайн WebApiClientGen и текстовый поток с использованием WebApiClientGen учитывает разработку на основе Trunk, которая более эффективна для непрерывной интеграции, чем другие разветвленные стратегии, такие как Feature Branching и Gitflow и т. д.
Чтобы следить за этим новым способом разработки клиентских программ, лучше, чтобы у вас был проект ASP.NET Web API или проект MVC, который содержит веб-API. Вы можете использовать существующий проект или создать демо-версию.
Использование кода
В этой статье рассматривается пример кода с помощью jQuery. Аналогичный пример кода для Angular 2 доступен в «ASP.NET Web API, Angular2, TypeScript и WebApiClientGen».
Шаг 0: установите пакет NuGet WebApiClientGen в проект веб-API
Установка также включает загрузку зависимых пакетов NuGet Fonlow.TypeScriptCodeDOM и Fonlow.Poco2Ts для ссылок проекта.
Пакет NuGet добавит 2 файла TS в папку проекта ~/Scripts/ClientApi. Один из них - HttpClient.ts, а другой - WebApiClientAuto.ts, и он будет заменяться каждый раз, когда выполняется CodeGen.
Кроме того, CodeGenController.cs добавляется в папку проекта Controllers для запуска CodeGen.
CodeGenController должен быть доступен только во время разработки в сборке отладки, поскольку клиентский 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
{
/// <summary>
/// Trigger the API to generate WebApiClientAuto.cs for an established client API project.
/// POST to http://localhost:10965/api/CodeGen with json object CodeGenParameters
/// </summary>
/// <param name="parameters"></param>
/// <returns>OK if OK</returns>
[HttpPost]
public string TriggerCodeGen(CodeGenParameters parameters)
{
...
}
}
Примечания
CodeGenController установлен в YourMvcOrWebApiProject/Controllers, хотя в проекте для MVC проект имеет API-интерфейс для производных классов ApiController. Однако, в основном хорошо, когда веб-API реализован в автономном проекте веб-API. И если вы хотите, чтобы проект MVC и проект веб-API выполнялись на одном и том же веб-сайте, вы можете просто установить веб-API в качестве приложения веб-сайта MVC.
Включить Doc комментарии веб-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» на странице проекта «Properties» установите флажок на «Output/XML Document File» и установите «bin\Your.WebApi.xml», а путь вывода - «bin» по умолчанию.
Если у вас есть другие сборки для моделей данных, вы можете сделать то же самое, чтобы обеспечить создание и копирование комментариев к клиентскому API.
Шаг 1: Подготовьте данные конфигурации 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 для POST в CodeGen Web API:
{
"ApiSelections": {
"ExcludedControllerNames": [
"DemoWebApi.Controllers.Account"
],
"DataModelAssemblyNames": [
"DemoWebApi.DemoData",
"DemoWebApi"
],
"CherryPickingMethods": 1
},
"ClientApiOutputs": {
"ClientLibraryProjectFolderName": "DemoWebApi.ClientApi",
"GenerateBothAsyncAndSync": true,
"CamelCase": true,
"TypeScriptJQFolder": "Scripts\\ClientApi",
"TypeScriptNG2Folder": "..\\DemoAngular2\\ClientApi"
}
}
Рекомендуется сохранить конфигурационные данные JSON в файл вроде того, что, расположен в папке проекта Web API.
Если у вас есть все классы POCO, определенные в проекте Web API, вы должны поместить имя сборки проекта веб-API в массив «DataModelAssemblyNames». Если у вас есть специальные сборки моделей данных для хорошего разделения понятий, вы должны поместить соответствующие имена сборки в массив.
«TypeScriptNG2Folder» - это абсолютный путь или относительный путь к проекту Angular2. Например, "..\\DemoAngular2\\ClientApi" указывает на проект Angular2 как на проект, созданный в качестве родственного проекту Web API .
CodeGen генерирует строго типизированные интерфейсы TypeScript из классов POCO в соответствии с «CherryPickingMethods», который описан в doc-комментарии ниже:
/// <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. Запустите проект DEBUG сборки веб-API
Шаг 4: данные POST JSON Config для запуска генерации клиентских API кодов.Запустите веб-проект в среде IDE на IIS Express.
Затем вы используете Curl или Poster или любые ваши любимые клиентские инструменты для POST для http://localhost:10965/api/CodeGen, with content-type=application/json.
Подсказки:
Таким образом, вам просто нужно выполнить шаг 2, чтобы генерировать клиентский API всякий раз, когда обновляется веб-API, поскольку вам не нужно устанавливать пакет NuGet или каждый раз создавать новые данные конфигурации JSON.
Вам не составит труда написать некоторые пакетные сценарии для запуска веб-API и данные конфигураций POST и JSON. И я на самом деле разработал его для вашего удобства: файл сценария Powershell, который запускает проект Web (API) в IIS Express, затем отправляет конфигурационный файл JSON для запуска генерации кода.
Эта диаграмма последовательности иллюстрирует цикл разработки:
Публикация библиотек клиентского API
Теперь у вас есть клиентский API в сгенерированном TypeScript, похожий на этот пример:
/// <reference path="../typings/jquery/jquery.d.ts" />
/// <reference path="HttpClient.ts" />
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<DemoWebApi_DemoData_Client.Address>;
}
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<Array<string>>;
Int3D?: Array<Array<Array<number>>>;
Lines?: Array<string>;
}
}
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<string>}
*/
Get(callback: (data : Array<string>) => 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);
}
}
}
Подсказки:
Если вы хотите, чтобы коды TypeScript, сгенерированные в соответствии с горбатым регистром javascript и JSON, вы можете добавить следующую строку в классе WebApiConfig для генерирующих кодов Web API:
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();
то имена свойств и имена функций будут в горбатом регистре, если соответствующие имена в C # находятся в регистре Pascal. Для получения дополнительной информации, пожалуйста, проверьте camelCasing или PascalCasing.
Внутреннее использование
Файл TypeScript WebApiClientAuto.ts, сгенерированный в ~Scripts/ClientApi, по умолчанию связан с вашим проектом MVC/Web API, поэтому его можно будет использовать в Visual Studio IDE во время разработки.
При написании клиентских кодов в некоторых достойных текстовых редакторах, таких как Visual Studio, вы можете получить хороший ввод с автодополнением.
Внешнее использование
Если вы предполагаете, что некоторые внешние разработчики будут использовать ваш веб-API через JavaScript, вы можете опубликовать созданный клиентский API-интерфейс TypeScript или скомпилированные файлы JavaScript вместе с страницами справки, сгенерированными структурой ASP.NET MVC.
Точки интересов
Хотя ASP.NET MVC и веб-API используют приложения NewtonSoft.Json для JSON, NewtonSoft.Json может хорошо управлять классами POCO, оформленных DataContractAttribute.
Пространства имен CLR будут переведены в пространства имен TypeScript путем замены точки на подчеркивание и добавления «Client» в качестве окончания. Например, пространство имен 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 вы должны знать о ошибках SOAP; и в веб-службах RESTful вам, возможно, придется иметь дело с кодами состояния HTTP и Response.
А шаблоны запросов дают мало смыслового значения функций API. В отличие от этого, WebApiClientGen называет функции клиента после функций службы, как это делает SvcUtil.exe в WCF по умолчанию, поэтому генерируемые клиентские функции имеют хорошее смысловое значение, если вы, как разработчики сервиса, дали имена сервисным функциям после получения хороших семантических имен.
В общей картине SDLC, охватывающей как сервисную, так и клиентскую разработку, разработчики услуг имеют представление о семантическом значении служебных функций, и, как правило, обладают хорошей практикой программирования для именования функций после функциональных описаний. Ресурсно-ориентированный CRUD может иметь семантическое значение или просто стать техническим переводом из функциональных описаний.
WebApiClientGen копирует комментарии вашего веб-API к комментариям JsDoc3 в сгенерированных кодах типа TypeScript, поэтому вам не нужно читать страницы справки, созданные MVC, и ваше клиенто-сервисное программирование станет более плавным.