TypeGen — Configuration & output
Layers (lowest → highest precedence)
Section titled “Layers (lowest → highest precedence)”- Defaults
- Global
TypeScript/OpenApi/Python/Zod/TanStackQueryblocks inITypeGenConfigurator ForType<T>()per-type fluent overrides- Class / property attributes (
[TsName],[OpenApiProperty], etc.)
Project-wide — ITypeGenConfigurator (fluent DSL)
Section titled “Project-wide — ITypeGenConfigurator (fluent DSL)”One class per project, picked up automatically by the generator. The Configure
method body is a fluent DSL parsed at compile time — it’s never actually invoked
at runtime, so all arguments must be literal expressions (string literals, enum
members, constants). Anything dynamic is invisible and surfaces as diagnostic
TG0013.
public sealed class TypeGenConfig : ITypeGenConfigurator{ public void Configure(ITypeGenBuilder b) { b.TypeScript(ts => { ts.OutputDir = "../client/src/api"; ts.FileLayout = TypeScriptFileLayout.SingleFile; ts.SingleFileName = "models.ts"; ts.PropertyNameStyle = NameStyle.CamelCase; });
b.OpenApi(oa => { oa.OutputPath = "../api/openapi.yaml"; oa.Title = "Order Service"; oa.Version = "2.1.0"; oa.Description = "Public API for the order service."; });
b.TanStackQuery(q => { q.OutputDir = "../client/src/api"; q.SingleFileName = "api.gen.ts"; q.BaseUrlExpression = "import.meta.env.VITE_API_URL"; // q.FileLayout = QueryFileLayout.SplitByTag; // q.ApiClientImportPath = "./http-client"; // q.ApiClientName = "request"; });
// Per-type overrides for DTOs you can't (or don't want to) annotate — // e.g. types from a referenced library. b.ForType<Order>() .TsName("OrderDto") .OutputDir("generated/orders");
b.ForType<InternalAudit>().Ignore();
// Fluent-only discovery — no [GenerateTypes] needed on the class. // .WithGeneratedTypes(targets) opts the type into emission for the listed // targets. Combine with the usual chain (TsName, .Property, etc.) to // tweak the output. b.ForType<Article>() .WithGeneratedTypes(TypeTarget.TypeScript | TypeTarget.OpenApi) .TsName("ArticleDto") .Property(p => p.Body).TsType("string | null"); }}For the emitted default TanStack Query fetch client, BaseUrlExpression is
evaluated safely. If the expression is unset, empty, or throws, the client falls
back to window.location.origin in browsers.
Discovery vs override. Without
.WithGeneratedTypes(...), the fluent block is a no-op for types that don’t carry[GenerateTypes]— the chain just sits there registering overrides for a class TypeGen never sees. Adding the marker method is the explicit “yes, emit code for this type” signal.
Targeting generic types — ForType(typeof(...))
Section titled “Targeting generic types — ForType(typeof(...))”Open generics can’t be passed as a C# type argument (ForType<Base<>>() is a
syntax error), so pair the second ForType overload with typeof(...):
// Open form — the canonical way to target every Base<T> instantiation at once.b.ForType(typeof(Base<>)) .Property("InternalTrace").TsIgnore() .Property("DebugToken").OpenApiIgnore();
// Closed form works too — both collapse onto the same Base<T> key that the// schema model uses, so a single line covers Base<int>, Base<string>, etc.b.ForType(typeof(Base<int>)) .TsName("BaseDto");Strongly-typed lambda selectors aren’t available on this path (the type
argument is erased) — use the string-based Property(name) overload. It’s
parsed as a literal; nameof(T.Member) also works since it’s a compile-time
constant.
b.ForType<Order>().Property(nameof(Order.Email)).TsName("emailAddress");Prefer a closed form for typed selectors. When you want refactor-safe
c => c.Propertylambdas on a generic, useb.ForType<Base<SomeT>>()with any valid closed instantiation — the parser normalizes to the openBase<T>key anyway, so one override covers every construction. Reach forForType(typeof(Base<>))only when no closed form satisfiesBase<T>’s type constraints (rare).
Targeting Dto-generated companion DTOs
Section titled “Targeting Dto-generated companion DTOs”When the parent type carries [CrudApi] (or [CreateDto]/[UpdateDto]/
[ResponseDto]), Dto generates Create{X}Request / Update{X}Request /
{X}Response companion records. Roslyn’s source-generator architecture
prevents TypeGen from resolving those records as symbols in the same
compilation pass — so you can’t write b.ForType<CreateArticleRequest>()
and expect the symbol to bind.
TypeGen handles this with a synthesis path: when the fluent type argument
matches the Create{X}Request / Update{X}Request / {X}Response naming
pattern AND the symbol is unresolvable, TypeGen looks for the parent type
X in the user’s assembly and emits a synthetic schema from its properties.
// Standalone — no [GenerateTypes] on Article needed, no anchor needed either.// Generates ONLY the Create variant as TS + OpenAPI; Update/Response are NOT// emitted because they aren't listed.b.ForType<CreateArticleRequest>() .WithGeneratedTypes(TypeTarget.TypeScript | TypeTarget.OpenApi) .TsName("ArticleDto");Per-companion fluent overrides (TsName, OpenApiName) apply on the
synthesized schema. The attribute path ([CrudApi] on parent, no fluent)
still emits all three companions automatically — fluent is for chirurgical
opt-in to a subset.
Only one
ITypeGenConfiguratorper project — multiple implementations fire diagnosticTG0010. Unrecognized fluent calls fireTG0012.
Per-class / per-property attributes
Section titled “Per-class / per-property attributes”Wins over project-wide settings and per-type fluent overrides. Precedence from
lowest to highest: defaults → TypeScript/OpenApi global blocks →
ForType<T>() per-type fluent → class/property attributes.
Output mechanism
Section titled “Output mechanism”TypeGen writes generated files to disk through two complementary paths:
- Live writes from the generator (default, on every IDE save). The source
generator itself calls
File.WriteAllTextfor each emitted file. Since Roslyn re-runs the generator on each compile cycle the IDE triggers, your.ts/.yaml/.pyfiles refresh as soon as you save the source. Wrapped intry/catch— sandboxed analyzer hosts (some Rider configs, restricted CI containers) silently fall back to path #2. - MSBuild post-build target (shipped in
build/ZibStack.NET.TypeGen.targets, auto-imported by the NuGet package). Reads a manifest.g.csthe generator emits toobj/generated/and writes the same set of files via an inlineRoslynCodeTaskFactoryC# task. Always runs ondotnet build, regardless of whether path #1 succeeded.
Both paths skip writes when content is byte-identical (mtime stays stable —
keeps file-watcher-driven dev servers like Vite from looping). Both sweep
stale files in the touched directories: any file carrying our @generated
banner that isn’t in the current emit set gets deleted, so renames don’t
leak orphaned outputs.
No companion task DLL, no reflection over the built assembly — the inline task is ~60
lines of C# directly in the .targets file. Files are skipped when their content is
unchanged (stable mtimes, no file-watcher thrash in frontend dev servers).