@massivescale/tsp-aspnetcore-api
v1.1.0
Published
TypeSpec emitter that generates C# models, ASP.NET Core controllers, service interfaces, and FluentValidation validators.
Maintainers
Readme
@massivescale/tsp-aspnetcore-api
A TypeSpec emitter that generates C# model classes, interfaces, enums, ASP.NET Core controllers, service interfaces, and FluentValidation validators from TypeSpec definitions.
For each TypeSpec model that is an HTTP payload (a request or response body, or reachable from one), the emitter produces a public partial class <Name>. A base model carrying @discriminator is emitted abstract, since it has no discriminator value of its own (see Model Generation). Response models (OkResponse-style models with an @body) and metadata-only models (all @header / @query / @path / @statusCode) describe the HTTP envelope rather than data, so no class is emitted for them. Each templated payload instantiation gets its own class (PagedResult<Widget> → PagedResultWidget). See Which models get a class. An optional companion public partial interface I<Name> can be enabled with emit-interfaces: true.
TypeSpec enum declarations and named string-literal union types become C# enums with [JsonConverter(typeof(EnumMemberConverterFactory))] and optional [EnumMember(Value = "...")] attributes.
When HTTP operations are present the emitter generates ASP.NET Core controllers and service interfaces. Controllers are emitted as abstract classes, and service interfaces are emitted as partial to enable extension and composition. PATCH operations receive a MergePatch<T> body (RFC 7396 JSON Merge Patch semantics) rather than a distinct update model — the same entity class is used for both POST and PATCH. MergePatch<T> exposes recursive Patch(T) / PatchAsync(T) updates and TryPatch rejected-path diagnostics, plus IsDefined / IsNull / TryGetValue helpers for fine-grained field-level control. FluentValidation validators enforce which fields are writable per operation, including recursively nested model properties. Explicit object-value defaults such as = #{} generate fresh nested model instances. Each output type can be disabled independently via the emit-* options.
Install
npm install --save-dev @massivescale/tsp-aspnetcore-apiPrerelease versions are published under the beta tag:
npm install --save-dev @massivescale/tsp-aspnetcore-api@betaThe emitter requires Node.js 22 or later and these peer dependencies:
npm install --save-dev @typespec/compiler @typespec/http @typespec/versioningQuick start
Add the emitter to tspconfig.yaml and run the compiler:
emit:
- "@massivescale/tsp-aspnetcore-api"
options:
"@massivescale/tsp-aspnetcore-api":
root-namespace: MyCompany.Apinpx tsp compile .With root-namespace: MyCompany.Api and the TypeSpec below, the emitter writes Models/Users/User.g.cs. Every model shares the MyCompany.Api.Models namespace; the Users folder mirrors the TypeSpec namespace below the root (see Namespace Resolution).
namespace MyCompany.Api.Users;
/** A registered user. */
model User {
@format("uuid") id: string;
name: string;
active: boolean;
joined?: utcDateTime;
}// Models/Users/User.g.cs (file header and #pragma lines omitted)
#nullable enable
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace MyCompany.Api.Models
{
/// <summary>
/// A registered user.
/// </summary>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public partial class User
{
[JsonPropertyName("id")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Guid? Id { get; set; }
[JsonPropertyName("name")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Name { get; set; }
[JsonPropertyName("active")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? Active { get; set; }
[JsonPropertyName("joined")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public DateTimeOffset? Joined { get; set; }
}
}Add @typespec/http operations to the spec and the emitter also writes abstract controllers and service interfaces. See Controllers and Services for that output and how to implement it, and example/aspnet-versioned for a complete ASP.NET Core project.
Options
| Option | Type | Default | Description |
| ----------------------------- | ------------------------------------------------------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| abstract-suffix | string | "Base" | Suffix appended to generated abstract controller class names, e.g. UsersControllerBase. |
| additional-usings | string[] | [] | Extra using directives added to every generated file. |
| cancellation-token | boolean | true | When true, adds CancellationToken cancellationToken to every controller action and service method, and emits using System.Threading; in controller and service files. |
| clean-output-dirs | boolean | true | When true, previously-generated files are removed from every output directory before emitting, so renamed or deleted types leave no stale files behind. Each run records the files it wrote in .tsp-aspnetcore-api-manifest (in the emitter output dir) and the next run deletes exactly those. Without a manifest, only files that match file-extension and carry this emitter's signature in their first lines are deleted; bin, obj, node_modules, dot-folders and links are never entered. See Output cleaning. |
| controller-query-params | "all" | "required" | "none" | "required" | Which @query parameters become controller action and service method parameters. "all" emits every query parameter, "required" emits only non-optional ones, and "none" emits none. Path and header parameters and the request body are always emitted. See Controllers and Services — Query parameters. |
| controllers-namespace | string | <root>.Controllers | Verbatim C# namespace for all generated controller files. When unset, defaults to <root-namespace>.Controllers. |
| controllers-output-dir | string | "Controllers" | Destination for generated controller files. |
| emit-controllers | boolean | true | When false, no controller base class files are emitted. |
| emit-helpers | boolean | false | When true, EnumMemberConverter.g.cs is emitted even when the spec has no enums. The converter is always emitted when any enum is generated, because every enum references it. MergePatch.g.cs is always emitted when merge-patch-style is "generic" (it is required by PATCH operations). |
| merge-patch-style | "generic" | "typed" | "generic" | Controls how the RFC 7396 Merge Patch support class is emitted. "generic" emits a single shared MergePatch<T> helper in the helpers directory. "typed" emits a concrete {Model}MergePatchUpdate class per entity in the models directory. Both expose the same API surface. See RFC 7396 Merge Patch. |
| emit-interfaces | boolean | false | When true, a companion I<Model> interface file is emitted for every model class. |
| emit-services | boolean | true | When false, no service interface files are emitted. |
| emit-validators | boolean | false | When true, FluentValidation validator classes are generated for models that appear as POST, PUT or PATCH request bodies. Discriminator fields from @discriminator hierarchies are excluded from validation rules. See FluentValidation validators. |
| file-extension | string | ".g.cs" | File extension for all generated files. Must be one or more .segment parts of letters, digits, _ or - (e.g. .g.cs, .generated.cs); an empty extension is rejected. |
| fully-qualified-types | boolean | false | When false, generated model, enum, and helper references use short names and generated files import the relevant namespaces; names that clash with framework types (Task, Version, …) stay qualified. Set to true to include namespaces in each type reference. See Cross-namespace references. |
| helpers-namespace | string | <root>.Helpers | Verbatim C# namespace for all generated helper files. When unset, defaults to <root-namespace>.Helpers. |
| helpers-output-dir | string | "Helpers" | Destination for generated helper files (MergePatch<T>, EnumMemberConverter). |
| interfaces-output-dir | string | "Models" | Destination for generated interface files. |
| models-namespace | string | <root>.Models | Verbatim C# namespace for all generated model, interface, and enum files. Interfaces always share this namespace. When unset, defaults to <root-namespace>.Models. |
| models-output-dir | string | "Models" | Destination for generated class and enum files. Relative paths resolve against emitter-output-dir. |
| namespace-from-path | boolean | false | Controls file placement only — does not affect C# namespaces. When true, files go flat in their output directory. When false (default), files are placed in subdirectories derived from the TypeSpec namespace. See Namespace resolution. |
| namespace-map | Record<string, string> | {} | Rewrites TypeSpec namespace names for file placement (folder path computation). Does not affect verbatim C# namespaces. Longest-prefix match wins; sub-namespaces inherit the rewrite automatically. |
| nullable-properties | boolean | true | When true, all properties are emitted as nullable (string?, int?). When false, only TypeSpec-optional properties and T \| null unions are nullable. |
| root-namespace | string | (inferred) | Root C# namespace. Used as the prefix for all default section namespaces. Stripped from folder paths when namespace-from-path is false. When omitted, inferred from the TypeSpec namespace tree. |
| route-prefix | string | "api/{version}" | Prefix prepended to every controller route. The {version} token is replaced with the API version value (e.g. v1.0) for versioned services, and stripped for unversioned services. E.g. "api/{version}" + v1.0 → /api/v1.0/users. |
| services-namespace | string | <root>.Services | Verbatim C# namespace for all generated service interface files. When unset, defaults to <root-namespace>.Services. |
| services-output-dir | string | "Services" | Destination for generated service interface files. |
| templates | Record<string, string> | {} | Custom Handlebars template paths keyed by template name. See Custom templates. |
| validators | "post" | "patch" | "both" | "both" | Controls which validator type(s) are emitted per model. "post" emits {Model}Validator.g.cs; "patch" emits {Model}PatchValidator.g.cs; "both" emits both. |
| validators-namespace | string | <root>.Validators | Verbatim C# namespace for all generated validator files. When unset, defaults to <root-namespace>.Validators. |
| validators-output-dir | string | "Validators" | Output directory for generated validator and initializer files. |
| validators-version-strategy | "earliest" | "latest" | "per-version" | "version-aware" | (auto) | Controls how versioning affects validator generation. Auto-detected: "version-aware" when @versioned is present, "earliest" otherwise. See Version strategies. |
Documentation
- Type Mapping — TypeSpec-to-C# scalar and collection mappings,
@formatoverrides,@encodeencodings - Decorators — Emitter-specific decorators such as
@serverName - Model Generation — Which models get a class (response and metadata-only models, template instances), default property values, enums,
@discriminatorpolymorphism, cross-namespace references - Namespace Resolution — How C# namespaces are derived from output paths and TypeSpec namespaces
- Controllers and Services — ASP.NET Core controller and service interface generation
- RFC 7396 Merge Patch —
MergePatch<T>generic container, PATCH body generation, and validator integration - FluentValidation Validators — Validator generation, constraint mapping, version strategies
- Custom Templates — Handlebars template overrides and view model reference
- Output Cleaning — Removing stale generated files before emit, and its guardrails
- Development — Build, test, and project structure
Known limitations
- Models merge all API versions. A versioned spec produces one class per model containing every property from every version. Only routes and validators are version-aware; use the
validators-version-strategyoption to control how validation treats properties added or removed in later versions. - Multipart bodies are not supported.
@multipartBodyandHttpPart<T>operations are not mapped: the generated action and service method get no body parameter for them. @cookieparameters are not supported. Cookie parameters are left out of the generated action and service signatures; read them fromRequest.Cookiesin your controller.@queryarrays default toexplode: false. TypeSpec's default for array query parameters is the comma-separated form (?tags=a,b), but ASP.NET Core's[FromQuery]binding expects repeated keys (?tags=a&tags=b). The emitter ignoresexplode, so declare@query(#{ explode: true })to make the TypeSpec contract (and any OpenAPI generated from it) match what the controller binds, or add a custom model binder for the comma-separated form.
