Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions docs/src/content/docs/packages/typegen.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
---
title: TypeGen — TypeScript & OpenAPI from C#
description: Compile-time code generator that emits TypeScript interfaces and OpenAPI 3.0 schemas from C# DTOs. Roslyn-native, zero reflection, no running app required. One [GenerateTypes] attribute on a class, and `dotnet build` writes the .ts and .yaml files into your configured output directory.
title: TypeGen — TypeScript, OpenAPI & TanStack Query from C#
description: Compile-time code generator that emits TypeScript interfaces, OpenAPI 3.0 schemas, and TanStack Query clients from C# DTOs/endpoints. Roslyn-native, zero reflection, no running app required. One [GenerateTypes] attribute on a class, and `dotnet build` writes generated files into your configured output directory.
---

[![NuGet](https://img.shields.io/nuget/v/ZibStack.NET.TypeGen.svg)](https://www.nuget.org/packages/ZibStack.NET.TypeGen) [![Source](https://img.shields.io/badge/source-GitHub-blue)](https://github.com/MistyKuu/ZibStack.NET/tree/master/packages/ZibStack.NET.TypeGen)

Roslyn source generator that turns C# DTOs into **TypeScript interfaces** and an
**OpenAPI 3.0 schema document** at compile time. One attribute on a class,
Roslyn source generator that turns C# DTOs and ASP.NET endpoints into
**TypeScript interfaces**, an **OpenAPI 3.0 schema document**, and optional
**TanStack Query** client helpers at compile time. One attribute on a class,
`dotnet build`, and the files land in your configured output directory.

> **Why not NSwag / Reinforced.Typings?** Both rely on reflection over the
Expand All @@ -29,6 +30,7 @@ Roslyn source generator that turns C# DTOs into **TypeScript interfaces** and an
- [Diagnostic reference](/ZibStack.NET/packages/typegen/diagnostics/) — every `TG00xx` ID
- [Validation → OpenAPI](/ZibStack.NET/packages/typegen/validation-mapping/) — DataAnnotations / `[Z…]` → schema constraints
- [Endpoint discovery](/ZibStack.NET/packages/typegen/endpoint-discovery/) — Minimal API scan, native controllers, `[CrudApi]` synthesis
- [TanStack Query emitter](/ZibStack.NET/packages/typegen/emitters/tanstack-query/) — typed fetch functions, query keys, options, hooks, and cache helpers
- [Polymorphism & interfaces](/ZibStack.NET/packages/typegen/polymorphism-and-interfaces/) — `[JsonPolymorphic]` discriminated unions, opt-in `EmitInterfaces`
- [Advanced type features](/ZibStack.NET/packages/typegen/advanced-types/) — `[JsonExtensionData]`, computed/immutable props, string-enum converters, transitive nested-type discovery, inheritance rules
- [Python emitter](/ZibStack.NET/packages/typegen/emitters/python/) — Pydantic v2 / dataclasses
Expand Down
12 changes: 11 additions & 1 deletion docs/src/content/docs/packages/typegen/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: "Project-wide ITypeGenConfigurator fluent DSL, open-generic targeti
## Layers (lowest → highest precedence)

1. Defaults
2. Global `TypeScript` / `OpenApi` / `Python` / `Zod` blocks in `ITypeGenConfigurator`
2. Global `TypeScript` / `OpenApi` / `Python` / `Zod` / `TanStackQuery` blocks in `ITypeGenConfigurator`
3. `ForType<T>()` per-type fluent overrides
4. Class / property attributes (`[TsName]`, `[OpenApiProperty]`, etc.)

Expand Down Expand Up @@ -39,6 +39,16 @@ public sealed class TypeGenConfig : ITypeGenConfigurator
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>()
Expand Down
238 changes: 238 additions & 0 deletions docs/src/content/docs/packages/typegen/emitters/tanstack-query.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
---
title: TypeGen - TanStack Query emitter
description: "TypeTarget.TanStackQuery emits typed TanStack Query clients from discovered ASP.NET endpoints: fetch functions, query keys, options factories, hooks, and cache helpers."
---

`TypeTarget.TanStackQuery` emits TypeScript client code for
`@tanstack/react-query` v5 from the same endpoint model used by TypeGen's
OpenAPI paths. It scans:

- hand-written Minimal APIs (`MapGet`, `MapPost`, `MapGroup`, `.WithName(...)`, `.WithTags(...)`)
- hand-written `[ApiController]` actions
- `[CrudApi]` synthesis from `ZibStack.NET.Dto`

Use it with `TypeTarget.TypeScript` so request and response model files are
generated in the same build.

## Install in the frontend

```bash
npm install @tanstack/react-query
```

## Minimal API setup

```csharp
using Microsoft.AspNetCore.Mvc;
using ZibStack.NET.Dto;
using ZibStack.NET.TypeGen;

var workflow = app.MapGroup("/api/workflow").WithTags("Workflow");

workflow.MapGet("/workspaces/{workspaceId:guid}/items",
(Guid workspaceId,
[FromQuery] string? search,
[FromQuery] WorkItemState? state,
[FromQuery] string[]? labels,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20) =>
PaginatedResponse<WorkItemSummary>.Create(items, items.Count, page, pageSize))
.WithName("searchWorkItems")
.WithTags("Workflow");

workflow.MapPost("/workspaces/{workspaceId:guid}/items",
(Guid workspaceId, [FromBody] CreateWorkItemCommand body) => CreateItem(body))
.WithName("createWorkItem")
.WithTags("Workflow");
```

```csharp
[GenerateTypes(Targets = TypeTarget.TypeScript
| TypeTarget.OpenApi
| TypeTarget.TanStackQuery,
OutputDir = "../client/src/api")]
public class WorkItemSummary
{
public Guid Id { get; set; }
public required string Title { get; set; } = "";
public WorkItemState State { get; set; }
public List<string> Labels { get; set; } = new();
}

[GenerateTypes(Targets = TypeTarget.TypeScript
| TypeTarget.OpenApi
| TypeTarget.TanStackQuery,
OutputDir = "../client/src/api")]
public class CreateWorkItemCommand
{
public required string Title { get; set; } = "";
public WorkItemState InitialState { get; set; }
}
```

## Configuration

```csharp
public sealed class TypeGenConfig : ITypeGenConfigurator
{
public void Configure(ITypeGenBuilder b)
{
b.TypeScript(ts =>
{
ts.OutputDir = "../client/src/api";
ts.PropertyNameStyle = NameStyle.CamelCase;
});

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";
});
}
}
```

## Generated shape

```typescript
import { mutationOptions, queryOptions, useMutation, useQuery, useQueryClient, type QueryClient } from '@tanstack/react-query';
import type { CreateWorkItemCommand } from './CreateWorkItemCommand';
import type { WorkItemState } from './WorkItemState';
import type { WorkItemSummary } from './WorkItemSummary';

export type PaginatedResponseOfWorkItemSummary = {
items: WorkItemSummary[];
totalCount: number;
page: number;
pageSize: number;
totalPages: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
};

export const workflowKeys = {
all: ['workflow'] as const,
searchWorkItems: (input: SearchWorkItemsInput) => [...workflowKeys.all, 'searchWorkItems', input] as const,
};

export type SearchWorkItemsInput = {
workspaceId: string;
search?: string;
state?: WorkItemState;
labels?: string[];
page?: number;
pageSize?: number;
};

export function searchWorkItems(input: SearchWorkItemsInput, signal?: AbortSignal): Promise<PaginatedResponseOfWorkItemSummary> {
return apiFetch<PaginatedResponseOfWorkItemSummary>(`/api/workflow/workspaces/${encodeURIComponent(String(input.workspaceId))}/items`, {
method: 'GET',
query: {
search: input.search,
state: input.state,
labels: input.labels,
page: input.page,
pageSize: input.pageSize,
},
signal,
});
}

export function searchWorkItemsOptions(input: SearchWorkItemsInput) {
return queryOptions({
queryKey: workflowKeys.searchWorkItems(input),
queryFn: ({ signal }) => searchWorkItems(input, signal),
});
}

export function useSearchWorkItems(input: SearchWorkItemsInput) {
return useQuery(searchWorkItemsOptions(input));
}
```

Mutations get a fetch function, `mutationOptions`, a React hook, and tag-wide
invalidation:

```typescript
export type CreateWorkItemInput = {
workspaceId: string;
body: CreateWorkItemCommand;
};

export function createWorkItem(input: CreateWorkItemInput, signal?: AbortSignal): Promise<WorkItemSummary> {
return apiFetch<WorkItemSummary>(`/api/workflow/workspaces/${encodeURIComponent(String(input.workspaceId))}/items`, {
method: 'POST',
body: input.body,
signal,
});
}

export function useCreateWorkItem() {
const queryClient = useQueryClient();
return useMutation({
...createWorkItemMutationOptions(),
onSuccess: async () => {
await invalidateWorkflowQueries(queryClient);
},
});
}

export function invalidateWorkflowQueries(queryClient: QueryClient) {
return queryClient.invalidateQueries({ queryKey: workflowKeys.all });
}
```

## Output settings

| Setting | Default | Purpose |
|---|---|---|
| `OutputDir` | TypeScript output dir, then first model output dir | Where query files are written |
| `FileLayout` | `QueryFileLayout.SingleFile` | `SingleFile` or `SplitByTag` |
| `SingleFileName` | `api.gen.ts` | File name for single-file mode |
| `BaseUrlExpression` | `import.meta.env.VITE_API_URL` | Base URL expression used by the default fetch client |
| `ApiClientImportPath` | `null` | Import a custom client instead of emitting `apiFetch` |
| `ApiClientName` | `apiFetch` | Default or imported client function name |
| `ModelsImportPath` | computed | Force model type imports from one module |
| `EmitQueryOptions` | `true` | Emit `queryOptions(...)` helpers |
| `EmitMutationOptions` | `true` | Emit `mutationOptions(...)` helpers |
| `EmitHooks` | `true` | Emit `useQuery` / `useMutation` wrappers |
| `EmitCacheHelpers` | `true` | Emit invalidation and prefetch helpers |

## Custom fetch client

Set `ApiClientImportPath` when your app already has auth, retry, tenant, or
observability behavior in one HTTP client:

```csharp
b.TanStackQuery(q =>
{
q.ApiClientImportPath = "@/lib/api-client";
q.ApiClientName = "request";
});
```

The imported function is called like this:

```typescript
request<T>(path, {
method,
query,
headers,
body,
signal,
});
```

`query` values can be scalar or arrays. The generated default client appends
arrays as repeated query-string keys and JSON-serializes request bodies.

## Naming

For Minimal APIs, prefer `.WithName("searchWorkItems")` and `.WithTags("Workflow")`.
The operation name becomes the function/options/hook base name, and the tag
becomes the query-key group. Without `.WithName(...)`, TypeGen derives a stable
name from verb plus route segments.
21 changes: 13 additions & 8 deletions docs/src/content/docs/packages/typegen/endpoint-discovery.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
---
title: TypeGen — Endpoint discovery (OpenAPI `paths:`)
description: "Three ways TypeGen populates the OpenAPI paths block — hand-written Minimal API scan, native [ApiController] scan, and [CrudApi] synthesis. All unified, with collision rules."
title: TypeGen — Endpoint discovery
description: "Three ways TypeGen discovers endpoints for OpenAPI paths and TanStack Query clients — hand-written Minimal API scan, native [ApiController] scan, and [CrudApi] synthesis. All unified, with collision rules."
---

TypeGen populates the OpenAPI `paths:` block from three sources, merged into
one unified output. Hand-written code is always ground truth — when sources
collide on the same (verb, path), the native handler wins over synthesis.
TypeGen populates its endpoint model from three sources, merged into one unified
output. OpenAPI uses it for `paths:`; the TanStack Query emitter uses it for
client functions, keys, hooks, and cache helpers. Hand-written code is always
ground truth — when sources collide on the same (verb, path), the native handler
wins over synthesis.

## Hand-written Minimal API → OpenAPI `paths:`

Expand Down Expand Up @@ -40,6 +42,9 @@ lambda body.
var g = app.MapGroup("/api/widgets");
g.MapGet("/{id}", (int id) => ...); // emits /api/widgets/{id}
```
- **Endpoint names and tags** from `.WithName("operationId")` and
`.WithTags("Tag")`. These feed OpenAPI `operationId`/`tags` and TanStack
Query function/key names.
- **Parameter binding**: explicit `[FromRoute]` / `[FromBody]` / `[FromQuery]` /
`[FromHeader]` first; fallback to ASP.NET convention. `CancellationToken` /
`HttpContext` / `[FromServices]` params are filtered out.
Expand All @@ -57,9 +62,9 @@ lambda body.
extracted yet (uses the raw `Ok<T>` return type which reads as `IResult`)
- Endpoint filters chained via `.AddEndpointFilter(...)` are ignored (they
don't change the contract, only runtime behaviour)
- Per-endpoint metadata extension methods (`.WithName("X").Produces<T>()`) aren't
read — use the handler's actual return type or add `[CrudApi]` on the DTO
if you need fine control over the emitted shape
- Response metadata extension methods such as `.Produces<T>()` aren't read yet —
use the handler's actual return type or add `[CrudApi]` on the DTO if you need
fine control over the emitted shape

## Hand-written controllers → OpenAPI `paths:`

Expand Down
16 changes: 11 additions & 5 deletions packages/ZibStack.NET.TypeGen/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# ZibStack.NET.TypeGen

Roslyn source generator that emits **TypeScript** (`.ts`) and **OpenAPI 3.0**
(`.yaml` / `.json`) from C# DTOs annotated with `[GenerateTypes]`. Optional
**Python** (Pydantic v2 / dataclass) output. Compile-time only, zero reflection,
no running app required.
Roslyn source generator that emits **TypeScript** (`.ts`), **OpenAPI 3.0**
(`.yaml` / `.json`), and **TanStack Query** clients from C# DTOs/endpoints
annotated with `[GenerateTypes]`. Optional **Python** (Pydantic v2 / dataclass)
output. Compile-time only, zero reflection, no running app required.

## What it does

Expand All @@ -29,6 +29,9 @@ public enum OrderStatus { Pending, Shipped }
- `generated/openapi.yaml` — OpenAPI 3.0.3 schema with every class under
`components/schemas`, correct `$ref`s, and `nullable` / `required` / validation
constraints pulled from `ZibStack.NET.Validation` attributes.
- `generated/api.gen.ts` — optional TanStack Query v5 helpers from discovered
Minimal API, controller, and `[CrudApi]` endpoints: typed fetch functions,
query keys, options factories, hooks, and invalidation/prefetch helpers.

Nested types without their own `[GenerateTypes]` are auto-discovered by walking
the property graph, so you only annotate root aggregates.
Expand All @@ -49,6 +52,9 @@ the property graph, so you only annotate root aggregates.
(GET list, GET by id, POST, PATCH, DELETE) with the right request/response
schema refs, pagination wrappers, and query-string bindings for
`ZibStack.NET.Query` when referenced.
- **TanStack Query emitter** — generates React Query v5 clients from the same
endpoint discovery model as OpenAPI. Supports Minimal API `.WithName(...)` /
`.WithTags(...)`, custom API clients, split-by-tag output, and cache helpers.
- **Fluent project-wide config** via `ITypeGenConfigurator` — global output dir,
file layout (`FilePerClass` / `SingleFile`), naming styles, per-type overrides,
property-level overrides.
Expand All @@ -72,7 +78,7 @@ in the attribute / configurator surface.
## Docs

Full reference — type mapping, diagnostic list (`TG0001`-`TG0021`), fluent DSL,
`[CrudApi]` integration, Python emitter, file layout options — lives at
`[CrudApi]` integration, TanStack Query, Python/Zod emitters, file layout options — lives at
[mistykuu.github.io/ZibStack.NET/packages/typegen](https://mistykuu.github.io/ZibStack.NET/packages/typegen/).

## License
Expand Down
Loading