TypeContractor 0.16.0
dotnet tool install --global TypeContractor --version 0.16.0
dotnet new tool-manifest # if you are setting up this repo dotnet tool install --local TypeContractor --version 0.16.0
#tool dotnet:?package=TypeContractor&version=0.16.0
nuke :add-package TypeContractor --version 0.16.0
TypeContractor
Looks at one or more assemblies containing contracts and translates to TypeScript interfaces and enums
Goals
Take one or more assemblies and reflect to find relevant types and their dependencies that should get a TypeScript definition published.
Perform replacements on the names to strip away prefixes
MyCompany.SystemName.Common
is unnecessary to have in the output path. We can strip the common prefix and getapi/Modules/MyModule/SomeRequest.ts
instead ofapi/MyCompany/SystemName/Common/Modules/MyModule/SomeRequest.ts
.Map custom types to their TypeScript-friendly counterparts if necessary.
For example, say your system has a custom
Money
type that maps down tonumber
. If we don't configure that manually, it will create theMoney
interface, which only containsamount
as anumber
. That's both cumbersome to work with, as well as wrong, since the serialization will (most likely) serializeMoney
as anumber
.Help validate responses from the API
Regular
response.json()
returnsany
, which means that any return type we specify ourselves are just wishful thinking. It might match, it might differ a bit, or it can differ a lot. Using Zod, we generate schemas automatically that can be used for validating the response and knowing that we get the correct data back.Automatically generate API clients based on the controller endpoints.
Why bother creating a TypeScript API client where you have to manually keep routes, parameters and everything in sync? We have the data, we have the types, we have the schemas. Let's just make everything work together, and let TypeContractor handle keeping those pesky API changes in sync.
Setup and configuration
To accomplish the same configuration and results as described under Goals, create Contractor like this:
Contractor.FromDefaultConfiguration(configuration => configuration
.AddAssembly("MyCompany.SystemName.Common", "MyCompany.SystemName.Common.dll")
.AddCustomMap("MyCompany.SystemName.Common.Types.Money", DestinationTypes.Number)
.StripString("MyCompany.SystemName.Common")
.SetOutputDirectory(Path.Combine(Directory.GetCurrentDirectory(), "api")));
Configuration file
It's possible to create a configuration file and provide all the same options via a file instead of command line arguments.
Create a file called typecontractor.config
in the same or a parent directory
from where you run TypeContractor from. We use dotnet-config for parsing the
files (written in TOML), so it's possible to inherit from parent directories,
a user-wide configuration or a system-wide configuration. Or have a
repository-specific configuration that can be overridden inside the repo with
typecontractor.config.user
. You can edit the file manually or install the
dotnet-config tool for viewing and changing configuration similar to how
git config
works.
Example configuration file
[typecontractor]
# backslashes must be escaped
assembly = "bin\\Debug\\net8.0\\MyCompany.SystemName.dll"
output = "api"
strip = "MyCompany"
replace = "MyCompany.Common:CommonFiles" # Options can repeat
replace = "ThirdParty.Http:Http"
root = "~/api"
generate-api-clients = true
build-zod-schemas = true
Run manually
Get an instance of Contractor
and call contractor.Build();
Integrate with ASP.NET Core
The easiest way is to TypeContractor, using dotnet tool install --global typecontractor
. This adds typecontractor
as an executable installed on the
system and always available.
Run typecontractor
to get a list of available options.
This tool reflects over the main assembly provided and finds all controllers
(that inherits from Microsoft.AspNetCore.Mvc.ControllerBase
). Each controller
is reflected over in turn, and finds all public methods that returns
ActionResult<T>
. The ActionResult<T>
is unwrapped and the inner type T
is
added to a list of candidates.
Additionally, the public methods returning an ActionResult<T>
or a plain
ActionResult
will have their parameters analyzed as well. Anything that's not
a builtin type will be added to the list of candidates.
Meaning if you have a method looking like:
public async Task<ActionResult> Create([FromBody] CreateObjectDto request, CancellationToken cancellationToken)
{
...
}
we will add CreateObjectDto
to the list of candidates.
System.Threading.CancellationToken
is a builtin type (currently, this means
it is defined inside System.
) and will be ignored. Same with other basic
types such as int
, Guid
, IEnumerable<T>
and so on.
For each candidate, we apply stripping and replacements and custom mappings and write everything to the output files.
Installing locally
Instead of installing the tool globally, you can also add it locally to the project that is going to use it. This makes it easier to make sure everyone who wants to run the project have it available.
For the initial setup, run:
dotnet new tool-manifest
dotnet tool install typecontractor
in your Web-project.
Whenever new users check out the repository, they can run dotnet tool restore
and get everything you need installed.
Running automatically
In your Web.csproj
add a target that calls the tool after build. Example:
<Target Name="GenerateTypes" AfterTargets="Build">
<Message Importance="high" Text="Running in CI, not generating new types" Condition="'$(AGENT_ID)' != ''" />
<Message Importance="high" Text="Generating API types" Condition="'$(AGENT_ID)' == ''" />
<Exec
Condition="'$(AGENT_ID)' == ''"
ContinueOnError="true"
Command="typecontractor --assembly $(OutputPath)$(AssemblyName).dll --output $(MSBuildThisFileDirectory)\App\src\api --clean smart --replace My.Web.App.src.modules:App --replace Infrastructure.Common:Common --strip MyCompany" />
<Message Importance="high" Text="Finished generating API types" Condition="'$(AGENT_ID)' == ''" />
</Target>
This will only run in non-CI environments (tested on Azure DevOps). Adjust the environment variable as needed. You don't want new types to be generated on the build machine, that should use whatever existed when the developer did their thing.
It will first:
- Strip out
MyCompany.
from the beginning of namespaces - Replace
My.Web.App.src.modules
withApp
- Replace
Infrastructure.Common
withCommon
by looking at the configured assembly. The resulting files are placed in
Web\App\src\api
.
When running with --clean smart
, which is the default, it first generates the
updated or newly created files. After that, it looks in the output directory
and removes every file and directory that are no longer needed.
Other options are:
none
-- which as the name suggests, does no cleanup at all. That part is left as an exercise to the user.remove
-- which removes the entire output directory before starting the file generation. This is probably the quickest, but some tools that are watching for changes does not always react so well to having files suddenly disappear and reappear.
Integration with Zod
An experimental option to generate Zod schemas exists
behind the --build-zod-schemas
flag. This causes each generated TypeScript
file to also have a <TypeName>Schema
generated that can be integrated with
Zod. Currently no support for validations, but that might come in a future
update.
Example:
public class PaymentsPerYearResponse
{
public IEnumerable<int> Years { get; set; } = [2023, 2024];
public Dictionary<int, int> PaymentsPerYear { get; set; } = new Dictionary<int, int>
{
{ 2024, 4 },
{ 2023, 12 }
};
}
generates
import { z } from 'zod';
export interface PaymentsPerYearResponse {
years: number[];
paymentsPerYear: { [key: number]: number };
}
export const PaymentsPerYearResponseSchema = z.object({
years: z.array(z.number()),
paymentsPerYear: z.record(z.string(), z.number()), // JavaScript is very stringy (https://zod.dev/?id=records)
});
and can be integrated using something similar to
const response = await this.http.fetch('api/paymentsPerYear', { signal: cancellationToken });
const input = await response.json();
return PaymentsPerYearResponseSchema.parse(input);
which will throw a ZodError
if input
fails to parse against the schema.
Otherwise it returns a cleaned up version of input
.
Automatic API client generation
By running TypeContractor with the --generate-api-clients
flag, every
controller found will have an automatic client generated for each of the
discovered endpoints.
Should be combined with the --root
flag for generating nice imports.
For example --root "~/api"
if you are writing files to api
and have
~
defined as an alias in your bundler.
If Zod integration is enabled, the return type for each API call will
be automatically validated against the schema. If so, it expects
the Response
prototype to be extended with a parseJson
method.
An example implementation can be found in tools/response.ts
.
To provide a custom template instead of using the built-in Aurelia one,
provide --api-client-template
with the path to a Handlebars template
that does what you want. The available data model can be found in
TypeContractor/Templates/ApiClientTemplateDto.cs
and an example
template is TypeContractor/Templates/aurelia.hbs
.
Since the name is just the controller name with Controller
replaced
with Client
, collisions between controllers with the same name but
different namespaces are possible. If this happens, the first
controller found gets to keep the original name and TypeContractor
will prefix the colliding client with the last part of its namespace.
So for example:
ExampleApp.Controllers.DataController
turns into DataClient
,
while ExampleApp.Controllers.Subsystem.DataController
collides and
gets turned into SubsystemDataClient
.
Further customization using Annotations
To further customize the output of TypeContractor, you can install
the optional package TypeContractor.Annotations
and start annotating
your controllers.
Available annotations:
TypeContractorIgnore
: If you have a controller that doesn't need a client generated, you can annotate that controller usingTypeContractorIgnore
and it will be automatically skipped.TypeContractorName
: If you have a badly named controller that you can't rename, you want something custom, or just don't like the default naming scheme, you can apply this attribute to select a brand new name.If you have multiple endpoints with the same name and different parameters, C# handles the overloads perfectly, but not so much in TypeScript. Use
TypeContractorName
to rename a single endpoint to whatever fits.TypeContractorNullable
: If your project doesn't support nullable reference types, or you just feel like you know better, you can mark a property as nullable and override the automatically detected setting.
Future improvements
- Better documentation
- Better performance -- if this should run on build, it can't take forever
- Possible to add types to exclude?
- Improve method for finding AspNetCore framework DLLs
- Possible to provide a manual path, so not a priority
- Work with Hot Reload?
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. |
This package has no dependencies.
Version | Downloads | Last updated |
---|---|---|
0.16.0 | 73 | 12/17/2024 |
0.15.0 | 81 | 12/2/2024 |
0.14.0 | 97 | 11/17/2024 |
0.13.1 | 109 | 11/11/2024 |
0.13.0 | 93 | 11/9/2024 |
0.12.7 | 118 | 10/10/2024 |
0.12.6 | 109 | 9/18/2024 |
0.12.5 | 93 | 9/18/2024 |
0.12.4 | 97 | 9/18/2024 |
0.12.3 | 101 | 9/17/2024 |
0.12.2 | 102 | 9/16/2024 |
0.12.1 | 97 | 9/13/2024 |
0.12.0 | 109 | 9/13/2024 |
0.12.0-pre.4 | 65 | 8/30/2024 |
0.12.0-pre.3 | 50 | 8/29/2024 |
0.12.0-pre.2 | 51 | 8/28/2024 |
0.12.0-pre.1 | 60 | 8/28/2024 |
0.11.0 | 144 | 6/21/2024 |
0.11.0-pre.2 | 59 | 4/28/2024 |
0.11.0-pre.1 | 68 | 4/27/2024 |
0.10.0 | 144 | 4/21/2024 |
0.9.2 | 215 | 1/28/2024 |
0.9.1 | 208 | 1/7/2024 |
0.9.0 | 214 | 1/5/2024 |
0.8.1 | 199 | 12/24/2023 |
0.8.0 | 244 | 12/5/2023 |
0.7.0 | 244 | 9/17/2023 |
0.6.0 | 263 | 9/2/2023 |