📜 ⬆️ ⬇️

Towards Full Typing with TypeScript, Swashbuckle, and AutoRest

Introduction


This article discusses how to implement typed messaging between the Back-End based on ASP.NET Web API and Front-End created using TypeScript. This is of particular importance when working on bulk projects, and more importantly, if the team is distributed. For example, when Back-End and Front-End developers work from different places, in different time zones, and do not always have the opportunity to contact and discuss something. In this case, change tracking is a painstaking job that can be fraught with many subtle errors.

For the author of the article, as for the person who came to the development of the Front-End by WPF and Silverlight, the big problem was the lack of static typing. How many times instead of adding “2” and “2” added “2” and “Function returning 2”, or passed a DOM object instead of its jQuery wrapper. The emergence of static code analyzers, such as JSLint, somewhat eased the problem, but TypeScript was a real breakthrough, especially in team development.



')

The essence of the problem


TypeScript is a language that allows you to achieve static typing, although some people call it “illusion” ( habrahabr.ru/post/258957 , habrahabr.ru/post/272055 ). It is curious that critics emphasize working with the Back-End as a typically non-secure scenario.
However, the essence of the problem lies in the fact that when writing Front-End applications in JavaScript before, and in TypeScript at the present time, we do not have such tools for working with metadata and auto-generating client code, as we once had in WCF.

Metadata


If we look at the experience of WPF + WCF, then everything is quite good in this respect. Of course, the data, generally speaking, always travel in an untyped form, but when sent, it remains typed almost to the very end, and only immediately before sending to the other side are serialized into a string or binary stream. On the other side, again, they end up in some kind of customer who turns them into typed ones. In order not to write with the hands of such a client and not try to catch multiple errors, and there is metadata. In the .NET world, in 90% of cases, no work is required at all, neither for their generation, nor for their processing. You just write your service, do not forget to add the appropriate endpoint, and get auto-generated metadata. Also in one click you generate a client and as a result you get an exchange of typed messages.

When developing Single Page Application on JavaScript / TypeScript, the Web API replaces WCF. At one time, it was somewhat surprising why there is no way to generate metadata for the Web API out of the box (not to consider the same help-pages as metadata). Apparently the answer is that the main recipient of the data from the Web API was JavaScript code, in which typing does not make sense. However, we now have not JavaScript but TypeScript, and the desire to operate with typed data again becomes relevant.

A very popular metadata format is now OpenAPI / Swagger. Not surprisingly, there are opportunities to generate metadata and documentation in this format.

Next, we will demonstrate the process of organizing typed interaction.
In short, we will complete the following steps:

  1. Let's connect and configure the Swashbuckle library
  2. Generate documentation / metadata
  3. Make sure that the generated file is stored in the version control system
  4. Connect AutoRest
  5. Generate client models
  6. Let's test them in action.


Swashbuckle


github.com/domaindrivendev/Swashbuckle

First we want to generate metadata.
So, suppose we have a Web API, and in it is a controller responsible for working with employees.
/// <summary> /// Gets all employees. /// </summary> /// <remarks> /// Gets the list of all employees. /// </remarks> /// <returns> /// The list of employees. /// </returns> [Route("api/employees")] [HttpGet] public Employee[] GetEmployees() { return new[] { new Employee { Id = 1, Name = "John Doe" }, new Employee { Id = 2, Name = "Jane Doe" } }; } 

As you can see, an array of typed objects of type Employee is returned. Having launched our project, we can request a list of employees:
http: // localhost: 1234 / api / employees

Let's connect the Swashbuckle library now. There are two Swashbuckle.Core and Swashbuckle packages in NuGet. The difference between the two is that the first is the kernel and contains all the code that does the magic, and the second, in turn, is just the add-on that installs the bootstrapper that configures the kernel.

This is written in the documentation github.com/domaindrivendev/Swashbuckle#iis-hosted

We prefer installing Core and writing the configuration code ourselves, since it is then more convenient to reuse.

Let's install it
 PM> Install-Package Swashbuckle.Core 

register with WebActivatorEx
 [assembly: WebActivatorEx.PreApplicationStartMethod(typeof(FullyTypedExample.WebApi.SwaggerConfig), "RegisterGlobal")] 

and also write the configuration code
 /// <summary> /// Configures Swagger. /// </summary> /// <param name="config"> /// The Swagger configuration. /// </param> public static void ConfigureSwagger(SwaggerDocsConfig config) { config.SingleApiVersion("v1", "FullyTypedExample.WebApi"); config.IncludeXmlComments(GetXmlCommentsPathForControllers()); config.IncludeXmlComments(GetXmlCommentsPathForModels()); config.GroupActionsBy(apiDescription => apiDescription.ActionDescriptor.ControllerDescriptor.ControllerName); config.OrderActionGroupsBy(Comparer<string>.Default); config.PrettyPrint(); } 

Everything is quite simple here: first we install the version and title of our API. Further we say that it is necessary to include xml-comments for controllers and models. Adjust the order and grouping of action inside the swagger document. Separately, I would like to mention the PrettyPrint option. It includes JSON formatting for the swagger document. This option is useful in order to further store the documentation in the version control system and easily view its changes using any diff viewer.

Now you can run the project and see the Swagger interface.
http: // localhost: 1234 / swagger



Nearby you can look at the swagger document itself in the form of JSON.
http: // localhost: 1234 / swagger / docs / v1

Now we need to add the generated documentation to the version control system. Since Swashbuckle uses IApiExplorer under the hood, in order to generate a swagger file you will have to launch the Web API (for more information, see github.com/domaindrivendev/Swashbuckle/issues/559 ). That is, each time you want to generate documentation, you have to run the Web API and copy swagger / docs to a file manually. Of course, I want something more automated.

We solved this by launching the Web API in the form of a self-hosted application, sending the request to the endpoint swagger and not writing the response to the file. This is where it was useful to reuse the Swashbuckle configuration code. It looks like this:
 /// <summary> /// Generate Swagger JSON document. /// </summary> /// <param name="filePath"> /// The file path where to write the generated document. /// </param> private static void GenerateSwaggerJson(string filePath) { // Start OWIN host using (TestServer server = TestServer.Create<WebApiHostStartup>()) { HttpResponseMessage response = server.CreateRequest("/swagger/docs/v1").GetAsync().Result; string result = response.Content.ReadAsStringAsync().Result; string path = Path.GetFullPath(filePath); File.WriteAllText(path, result); } } 

Let's run all this now:
 nuget.exe restore "..\FullyTypedExample.sln" "C:\Program Files (x86)\MSBuild\12.0\bin\MSBuild.exe" "..\FullyTypedExample.WebApi.SelfHosted\FullyTypedExample.WebApi.SelfHosted.proj" /v:minimal "..\FullyTypedExample.WebApi.SelfHosted\bin\Debug\FullyTypedExample.WebApi.SelfHosted.exe" --swagger "swagger.json" 

So we got the swagger document as a JSON file and put it into the version control system. Now Front-End developers from our distributed team can easily track changes in endpoint s. Let's see what it looks like.

Suppose we added a new action to get an employee by his ID.
 /// <summary> /// Gets employee by id. /// </summary> /// <param name="employeeId"> /// The employee id. /// </param> /// <remarks> /// Gets the employee by specified id. /// </remarks> /// <returns> /// The <see cref="Employee"/>. /// </returns> [Route("api/employees/{employeeId:int}")] public Employee GetEmployeeById(int employeeId) { return this.GetEmployees().SingleOrDefault(x => x.Id == employeeId); } 

And re-generated swagger.json. Let's see what has changed


As you can see, for this action documentation appeared, which can be easily seen using the diff viewer. Thanks to the PrettyPrint option, it is formatted and easy to read.

AutoRest


So, the first part of our task is completed - we have metadata. How now to generate the client part, i.e. data types (obtained from the server) on the client side?

I must say that you can generate the code itself for requesting a Web API, it's just a little more complicated and requires more time-consuming work on configuring code generators or writing your own. Also, a lot depends on which libraries (be it jQuery, SuperAgent or even the new experimental Fetch API developer.mozilla.org/en/docs/Web/API/Fetch_API ) and approaches (Promises, Rx, etc.) you use in your client code.

For code generation, the following options exist:

1. Swagger Code Generator github.com/swagger-api/swagger-codegen
The official tool from the Swagger team, written in Java and requires the appropriate infrastructure. It can also run in Docker. True, the generation of JavaScript and themes TypeScript is absent. Although if you need to generate code, for example, in Java - this is your choice. For us, he did not fit for obvious reasons.

2. Swagger JS library github.com/swagger-api/swagger-js
Also an official tool from the Swagger team. Already warmer. Written in JS and generates JS code, respectively. Installed via npm or bower. The infrastructure is suitable for us, but alas, there is not the type generation itself.

3. Swagger to JS & Typescript Codegen github.com/wcandillon/swagger-js-codegen
The project was published a little later than we began to develop this approach. Perhaps in the near future this will be the most appropriate solution.

4. Write your bike code generator. In general, why not? But for a start, we decided that we would try AutoRest, and if it does not take off, or does not suit us with opportunities, we’ll write our own, with blackjack and ... Well, you understand.

5. AutoRest github.com/Azure/autorest
And finally, Microsoft's Azure AutoRest team. Now the current version is 0.15.0, and frankly it is not clear if this is considered to be a full release from them or not, but the Pre mark, like the previous ones, is not observed. In general, everything is simple, we installed and immediately generated * .d.ts files that we needed.

So let's go through the final part of our journey with this tool.

We connect AutoRest via NuGet:
 PM> Install-Package AutoRest 

The package is not placed in any particular project, a link to it is added for the entire solution.
 <?xml version="1.0" encoding="utf-8"?> <packages> <package id="AutoRest" version="0.15.0" /> </packages> 

The package has a console application AutoRest.exe, which, in fact, performs the generation. To run, we use the following script
 nuget.exe restore "..\FullyTypedExample.sln" "..\packages\AutoRest.0.15.0\tools\AutoRest.exe" -Input "swagger.json" -CodeGenerator NodeJS move "Generated\models\index.d.ts" "..\FullyTypedExample.HtmlApp\models.d.ts" 

At the entrance we submit our previously generated swagger.json, and at the output we get models \ index.d.ts - a file with models. We copy it in the client project.

Now in TypeScript we have the following model description:
 /** * @class * Initializes a new instance of the Employee class. * @constructor * Represents the employee. * @member {number} id Gets or sets the employee identifier. * * @member {string} name Gets or sets the employee name. * */ export interface Employee { id: number; name: string; } 

Let's try it out in the case:
 public makeRequest() { this.repository.getEmployees() .then((employees) => { // Generate html using tempalte string this.table.innerHTML = employees.reduce<string>((acc, x) => { return `${acc}<tr><td>${x.id}</td><td>${x.name}</td></tr>`; }, ''); }); } 

Here we refer to the id and name model fields. We intentionally lowered the request to the server because as we have said, it may depend on the libraries and approaches chosen.

If we try to access the age field, which does not exist, our TS code will not compile. If the field we addressed earlier disappears in the API, our code will not compile again. If new fields are added, we will immediately see it using the same diff. In addition, we automatically receive JSDoc documentation based on metadata. In general, all the delights of static typing is obvious.

ResponseType


Interestingly, if necessary, for documentation, you can specify a different type than the one that is returned. For example, this may be useful if you have legacy code that works with untyped DataSets; or if you return the IHttpActionResult from the controllers. Without affecting the implementation of the methods, we can mark them with the ResponseType attribute and develop special types

 /// <summary> /// Gets all departments. /// </summary> /// <remarks> /// Gets the list of all departments. /// </remarks> /// <returns> /// The list of departments. /// </returns> [Route("api/departments")] [HttpGet] [ResponseType(typeof(DepartmentsResponse))] public DataSet GetDepartments() { var dataTable = new DataTable("Departments"); dataTable.Columns.Add("Id", typeof(int)); dataTable.Columns.Add("Name", typeof(string)); dataTable.Rows.Add(1, "IT"); dataTable.Rows.Add(2, "Sales"); var dataSet = new DataSet(); dataSet.Tables.Add(dataTable); return dataSet; } 

to get typed models on the client side
 /** * @class * Initializes a new instance of the Department class. * @constructor * Represents the department. * @member {number} id Gets or sets the department identifier. * * @member {string} name Gets or sets the department name. * */ export interface Department { id: number; name: string; } 


Problems


First, the size of the models.d.ts file increases over time. While we have not engaged in splitting it into several subfiles, but it certainly will need to be done.

There may also be a problem with the incorrect generation of field names if non-standard notation is used, for example, if underscores are used. The LAST_NAME field from the C # code is generated in Swagger as lasT_NAME, and in TypeScrpt as lasTNAME.

 /// <summary> /// Gets or sets the last name. /// </summary> [Required] // ReSharper disable once InconsistentNaming public string LAST_NAME { get; set; } 

 "lasT_NAME": { "description": "Gets or sets the last name.", "type": "string" } 

 export interface Employee { id: number; name: string; firstName: string; lasTNAME: string; } 

Note that most minor problems are easily solved using the configuration and are not worth special mention.

Conclusion


This approach allowed us to organize the exchange of typed messages. At the same time, he provided for the typing of client models, reduced the likelihood of discrepancies between client and server code, made it easier to track changes in the API and models. A nice bonus was the convenient manual testing API with a built-in REST client and the ability to generate payload on the fly according to the scheme. Using this approach also helped to improve the interaction of Back-End and Front-End developers.

A working example can be seen here.
github.com/EBTRussia/fully-typed-example

Source: https://habr.com/ru/post/283042/


All Articles