-
Notifications
You must be signed in to change notification settings - Fork 24
Quick Start
This guide will show you how to set up TypeScript.ContractGenerator in your web application
You can find the code for this Quick Start on GitHub here: AspNetCoreExample.Api
First, let's create new AspNet WebApi project using template:
dotnet new webapi --output AspNetCoreExample.Api --framework net7.0
Then navigate to created project and add package reference to TypeScript.ContractGenerator:
dotnet add package SkbKontur.TypeScript.ContractGenerator
For TypeScript.ContractGenerator to know how to generate TypeScript, you need to create folder TypeScriptConfiguration
and two files:
-
TypesProvider
that tells generator what types it should generate, let's start with TypesProvider that returns only existingWeatherForecastController
:
public class TypesProvider : IRootTypesProvider
{
public ITypeInfo[] GetRootTypes()
{
return new[] {TypeInfo.From<WeatherForecastController>()};
}
}
-
CustomGenerator
that tells generator how types should be translated to TypeScript:
public class CustomGenerator : CustomTypeGenerator
{
public CustomGenerator()
{
var controllerBase = TypeInfo.From<ControllerBase>();
WithTypeLocationRule(t => controllerBase.IsAssignableFrom(t), t => $"Api/{t.Name.Replace("Controller", "Api")}")
.WithTypeLocationRule(t => !controllerBase.IsAssignableFrom(t), t => $"DataTypes/{t.Name}")
.WithTypeBuildingContext(t => controllerBase.IsAssignableFrom(t), (u, t) => new ApiControllerTypeBuildingContext(u, t));
}
}
Above we set up several rules for our generator:
- We should put types that extend
ControllerBase
to folder./Api
, the resulting file forWeatherForecastController
will be./Api/WeatherForecastApi.ts
- We should put everything else to
./DataTypes
folder, for example,WeaterForecast
will be put to./DataTypes/WeatherForecast.ts
- We should use
ApiControllerTypeBuildingContext
to generate api from inheritors ofControllerBase
Now we can generate some TypeScript:
- Install dotnet tool:
dotnet tool install --global SkbKontur.TypeScript.ContractGenerator.Cli
- Run it (don't forget to build our project beforehand):
dotnet ts-gen -a ./bin/Debug/net7.0/AspNetCoreExample.Api.dll -o output --nullabilityMode NullableReference
In output folder we should get several files with following structure:
├── output
│ ├── Api
│ │ ├── WeatherForecastApi.ts
│ ├── DataTypes
│ │ ├── DateOnly.ts
│ │ ├── DayOfWeek.ts
│ │ ├── WeatherForecast.ts
Let's analyze files in output folder.
-
WeatherForecastApi.ts
will contain classWeatherForecastApi
with api methods and interfaceIWeatherForecastApi
with same methods. MethodWeatherForecastController.Get
will translate into:
export class WeatherForecastApi extends ApiBase implements IWeatherForecastApi {
async get(): Promise<WeatherForecast[]> {
return this.makeGetRequest(`/WeatherForecast`, {
}, {
});
}
};
It's worth noting that by default generated file with api expects that output folder contains ./ApiBase/ApiBase.ts
file with several methods:
export class ApiBase {
public async makeGetRequest(url: string, queryParams: Record<string, any>, body: any): Promise<any> { ... }
public async makePostRequest(url: string, queryParams: Record<string, any>, body: any): Promise<any> { ... }
public async makePutRequest(url: string, queryParams: Record<string, any>, body: any): Promise<any> { ... }
public async makeDeleteRequest(url: string, queryParams: Record<string, any>, body: any): Promise<any> { ... }
}
Let's look into DataTypes folder next
- Here's what generated
WeatherForecast.ts
looks like:
C# | TypeScript |
---|---|
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => ...;
public string? Summary { get; set; }
} |
export type WeatherForecast = {
date: DateOnly;
temperatureC: number;
temperatureF: number;
summary?: null | string;
}; |
It's possible that we do not want to generate some properties, in that case we can use ContractGeneratorIgnore
attribute:
public class WeatherForecast
{
...
+ [ContractGeneratorIgnore]
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
...
}
Run ts-gen again after rebuilding to see that temperatureF
is not present
- Genetared
DayOfWeek.ts
looks like this:
export enum DayOfWeek {
Sunday = 'Sunday',
Monday = 'Monday',
Tuesday = 'Tuesday',
Wednesday = 'Wednesday',
Thursday = 'Thursday',
Friday = 'Friday',
Saturday = 'Saturday',
}
Enums are generated with string constants, that means that StringEnumConverter
should be used when dealing with JSON globally or [JsonConverter(typeof(StringEnumConverter))]
should be placed on enums used in API, this behaviour can be changed if necessary by making custom EnumTypeBuildingContext based on TypeScriptEnumTypeBuildingContext and providing it to our CustomGenerator
- Generated
DateOnly.ts
looks like this:
export type DateOnly = {
year: number;
month: number;
day: number;
dayOfWeek: DayOfWeek;
dayOfYear: number;
dayNumber: number;
};
Generating DateOnly
type makes little sense because in JavaScript Date type is different. We can tell generator to use our own date type:
public CustomGenerator()
{
var controllerBase = TypeInfo.From<ControllerBase>();
WithTypeLocationRule(...)
+ .WithTypeRedirect(TypeInfo.From<DateOnly>(), "DateOnly", @"DataTypes\DateOnly")
.WithTypeBuildingContext(...);
}
After that, rerun ts-gen to see that DateOnly
and DayOfWeek
types are not generated, instead import statement was added to WeatherForecast.ts:
/* eslint-disable */
// TypeScriptContractGenerator's generated content
+ import { DateOnly } from './DateOnly';
export type WeatherForecast = {
...
};
Now you need to add your own DateOnly type in ./DataTypes/DateOnly.ts
, for example:
export type DateOnly = (Date | string);
Let's add more methods to our WeatherForecastController
For convenience, you can start TypeScript.ContractGenerator in watch mode:
dotnet ts-gen -a ./bin/Debug/net7.0/AspNetCoreExample.Api.dll -o output --nullabilityMode NullableReference --watch
it will monitor folder with binaries and regenerate TypeScript on changes to it
let's add two methods and rebuild
public class WeatherForecastController : ControllerBase
{
...
[HttpPost("Update/{city}")]
public void Update(string city, [FromBody] WeatherForecast forecast, CancellationToken cancellationToken)
{
}
[HttpPost("~/[action]")]
public void Reset(int seed)
{
}
}
after build we will see new methods in WeatherForecastApi.ts:
async update(city: string, forecast: WeatherForecast): Promise<void> {
return this.makePostRequest(`/WeatherForecast/Update/${city}`, {
}, {
...forecast,
});
}
async reset(seed: number): Promise<void> {
return this.makePostRequest(`/Reset`, {
['seed']: seed,
}, {
});
}
Next, let's add method that downloads some file:
[HttpGet("{city}")]
public ActionResult Download(string city)
{
var forecast = new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(1)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
};
return File(JsonSerializer.SerializeToUtf8Bytes(forecast), "application/json");
}
Usually, we would like to do something like window.location = '/WeatherForecast/City'
for this method, so we only need to get link to method in api. We can use [UrlOnly]
attribute to achieve this:
+ [UrlOnly]
[HttpGet("{city}")]
public ActionResult Download(string city)
after rebuild, we will get this method in WeatherForecastApi.ts
getDownloadUrl(city: string): string {
return `/WeatherForecast/${city}`;
}
Let's add another controller:
public class User
{
public Guid Id { get; set; }
public string Name { get; set; }
}
[Route("v1/users")]
public class UserController : ControllerBase
{
[HttpPost]
public ActionResult CreateUser([FromBody] User user) { ... }
[HttpDelete("{userId:guid}")]
public ActionResult DeleteUser(Guid userId) { ... }
[HttpGet("{userId:guid}")]
public ActionResult<User> GetUser(Guid userId) { ... }
[HttpGet]
public ActionResult<User[]> SearchUsers([FromQuery] string name) { ... }
}
Don't forget to add it to our TypesProvider
public class TypesProvider : IRootTypesProvider
{
public ITypeInfo[] GetRootTypes()
{
return new[]
{
TypeInfo.From<WeatherForecastController>(),
+ TypeInfo.From<UserController>(),
};
}
}
After rebuild we will get new ./Api/UserApi.ts
file
/* eslint-disable */
// TypeScriptContractGenerator's generated content
import { User } from './../DataTypes/User';
import { ApiBase } from './../ApiBase/ApiBase';
export class UserApi extends ApiBase implements IUserApi {
async createUser(user: User): Promise<void> {
return this.makePostRequest(`/v1/users`, {}, {...user});
}
async deleteUser(userId: string): Promise<void> {
return this.makeDeleteRequest(`/v1/users/${userId}`, {}, {});
}
async getUser(userId: string): Promise<User> {
return this.makeGetRequest(`/v1/users/${userId}`, {}, {});
}
async searchUsers(name: string): Promise<User[]> {
return this.makeGetRequest(`/v1/users`, {['name']: name}, {});
}
};
export interface IUserApi {
createUser(user: User): Promise<void>;
deleteUser(userId: string): Promise<void>;
getUser(userId: string): Promise<User>;
searchUsers(name: string): Promise<User[]>;
}
and ./DataTypes/User.ts
file
/* eslint-disable */
// TypeScriptContractGenerator's generated content
export type User = {
id: string;
name: string;
};