Three modes
C# contracts → TS, C# controllers → TS, or OpenAPI → C# contracts. Use whichever fits your team.
No drift, no schema files, no codegen config.

// Enums → string union types
public enum Priority { Low, Medium, High, Critical }
public enum WorkItemStatus { Draft, Open, InProgress, Review, Done, Cancelled }
// Multi-property records → object types
public sealed record Label(string Name, string Color);
// Single-property records → branded primitives
public sealed record Email(string Value);
public sealed record TaskId(Guid Value);// Nested records with nullables, enums, and value objects
public sealed record TaskDetailDto(
Guid Id,
string Title,
string? Description, // → string | null
WorkItemStatus Status, // → "Draft" | "Open" | ...
Priority Priority, // → "Low" | "Medium" | ...
string? AssigneeName,
List<Label> Labels, // → Label[]
List<CommentDto> Comments,
DateTime CreatedAt, // → string (ISO 8601)
DateTime? CompletedAt); // → string | null
// Generic wrapper — preserved as generic in TS
[RivetType]
public sealed record PagedResult<T>(
List<T> Items, int TotalCount, int Page, int PageSize);// Generated by Rivet — do not edit
export type Priority = "Low" | "Medium" | "High" | "Critical";
export type WorkItemStatus = "Draft" | "Open" | "InProgress"
| "Review" | "Done" | "Cancelled";
export type Label = {
name: string;
color: string;
};
export type Email = string & { readonly __brand: "Email" };
export type TaskId = string & { readonly __brand: "TaskId" };// Generated by Rivet — do not edit
import type { Label, Priority, WorkItemStatus } from "./common.js";
export type TaskDetailDto = {
id: string;
title: string;
description: string | null;
status: WorkItemStatus;
priority: Priority;
assigneeName: string | null;
labels: Label[];
comments: CommentDto[];
createdAt: string;
completedAt: string | null;
};// Generated by Rivet — do not edit
export type PagedResult<T> = {
items: T[];
totalCount: number;
page: number;
pageSize: number;
};[RivetClient] // ← auto-discover all endpoints
[Route("api/tasks")]
public sealed class TasksController(CreateTaskUseCase createTask) : ControllerBase
{
[HttpGet] // GET /api/tasks?page=&pageSize=&status=
[ProducesResponseType(typeof(PagedResult<TaskListItemDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> List(
[FromQuery] int page, [FromQuery] int pageSize,
[FromQuery] string? status, CancellationToken ct) { ... }
[HttpGet("{id:guid}")] // GET /api/tasks/{id} — two possible responses
[ProducesResponseType(typeof(TaskDetailDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(NotFoundDto), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Get(Guid id, CancellationToken ct) { ... }
[HttpPost] // POST /api/tasks — typed body + 201 response
[ProducesResponseType(typeof(CreateTaskResult), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> Create(
[FromBody] CreateTaskCommand command, CancellationToken ct) { ... }
[HttpDelete("{id:guid}")] // DELETE → renamed to remove() in TS
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct) { ... }
}// Generated by Rivet — do not edit
// Typed query params, fully unwrapped return type
export function list(
page: number, pageSize: number, status: string | null
): Promise<PagedResult<TaskListItemDto>>;
// Discriminated union for multi-response endpoints
export type GetResult =
| { status: 200; data: TaskDetailDto; response: Response }
| { status: 404; data: NotFoundDto; response: Response };
export function get(id: string): Promise<TaskDetailDto>;
export function get(id: string, opts: { unwrap: false }): Promise<GetResult>;
// Typed request body, 201 default
export function create(command: CreateTaskCommand): Promise<CreateTaskResult>;
// delete → remove (reserved word in TS)
export function remove(id: string): Promise<void>;oRPC gives you end-to-end type safety when your server is TypeScript. Rivet gives you the same DX when your server is .NET.
Unlike OpenAPI-based generators (NSwag, Kiota, Kubb), Rivet reads Roslyn's full type graph — nullable annotations, sealed records, string enum unions, generic type parameters — and produces richer TypeScript types than any JSON schema intermediary can represent.
Rivet is not just a client generator. Every type reachable from an endpoint or contract — records, enums, value objects, generics — is emitted automatically. No per-type annotations needed. For types that aren't reachable from any endpoint (shared frontend-only DTOs), [RivetType] opts them in explicitly.