npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@olivervea/typespec-csharp

v0.63.0

Published

TypeSpec emitter: an HTTP contract → its C# minimal-API surface (records, response unions, handler interfaces, route mapping, validators, JSON source-gen context) on the Olve.TypeSpec runtime.

Readme

@olivervea/typespec-csharp

npm

A TypeSpec emitter: an HTTP contract → its C# minimal-API surface, on the Olve.TypeSpec runtime. The service compiles the generated files and hand-writes one handler class per operation; the contract is the only place the API is defined.

tsp compile writes three files, named after the service namespace (namespace Widgets; below; the prefix option overrides it):

| File | Contents | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | WidgetsModels.g.cs | Records (request shapes split by @visibility), enums, @discriminated unions, @events unions (IServerSentEvent), and an IValidator<T> per request body with constraints (@minLength, @maxValue, …). | | WidgetsApi.g.cs | Per operation: a request record, a response union with one variant per declared status, and I…Handler : IOperationHandler<…>. Plus WidgetsOperations (declared statuses), WidgetsApi.HandlerTypes, AddWidgetsApi(), UseWidgetsApi(), MapWidgetsApi(). | | WidgetsJsonContext.g.cs | A System.Text.Json source-generation context for every DTO (Native AOT ready). |

Install

npm install --save-dev @olivervea/typespec-csharp @typespec/compiler @typespec/http @typespec/openapi3
dotnet add package Olve.TypeSpec

Configure

In tspconfig.yaml:

emit:
  - "@typespec/openapi3"
  - "@olivervea/typespec-csharp"
options:
  "@olivervea/typespec-csharp":
    emitter-output-dir: "{project-root}/artifacts/generated"
    namespace: "MyService.Api" # default: Generated.Api
    prefix: "MyService" # default: the service namespace's name → MyServiceApi, AddMyServiceApi, …
    external-types: # spec models the app already has a C# type for
      ErrorEnvelope: "MyService.Api.ErrorEnvelope"

Generated files belong in a gitignored folder the service's project compiles; Olve.TypeSpec's MSBuild targets run tsp compile before the build (<TypeSpecContract Include="spec/main.tsp" GeneratedDir="…" />, see its README).

What the contract gives you

  • Response unions. op update(...): Widget | NotFound | Conflict becomes WidgetsUpdateResponse with Ok(Widget), NotFound(ErrorEnvelope) and Conflict(ErrorEnvelope) variants. A handler returns one; an undeclared status can't compile. A success body no other variant carries converts implicitly (return widget;).
  • Several success statuses. (CreatedResponse & { @body body: Widget }) | (AcceptedResponse & …) is Created and Accepted, each writing its own status.
  • Visibility. @visibility(Lifecycle.Read) properties leave the request shapes (WidgetCreate, WidgetWritable); the response keeps them.
  • Discriminated unions (@discriminated(#{ envelope: "none", discriminatorPropertyName: "kind" })) become an abstract record with [JsonPolymorphic]/[JsonDerivedType]; the discriminator may appear anywhere in a request object.
  • Parameters bind from path, query and header; string enums by wire value (?color=red); @query(#{ explode: false }) string lists from one comma-separated value.
  • SSE. SSEStream<Events> of an @events union streams ServerSentEvent<Events> items: the event name is the variant's type literal, and the emitter refuses a variant whose type disagrees with its name.
  • Files and text. A bytes body with a non-JSON content type is a Stream, copied to the response as it is read (never buffered); a text/* string is written as is. A literal content type is a constant; for bytes, @header contentType: string makes it a ContentType member the handler sets.
  • Response headers. Each @header of a response is a member of its variant (optional ones last, null = not sent); string literal headers are constants. @head operations answer headers only.
  • Catch-all paths. @route("{+path}") as the last segment maps to {**path}. OpenAPI can't express reserved expansion (suppress @typespec/openapi3/path-reserved-expansion), so generated clients send a%2Fb; the binding accepts it as a/b and refuses a . or .. segment (a binding failure).
  • Raw request bodies. A bytes body with a non-JSON content type (@header contentType: "*/*" for any) is the request stream, unread, so a webhook signature over the exact bytes verifies. The handler reads it with its own size limit.
  • Typed ids (opt-in). A uuid is Guid unless tagged. @id(Model) on a uuid-format string scalar makes every property and path/query parameter of that scalar Olve.Utilities' Id<T> (nullable and arrays included); T is the model's generated record, or the C# type given as the second argument. See Typed ids.
  • Validation. Constraints on a request body become a validator that runs before the handler; failures and binding failures are answered by the app's IApiErrorWriter, in the contract's terms (the matched operation's declared statuses are at hand).

Unsupported shapes are reported as @olivervea/typespec-csharp/unsupported-type or unsupported-operation warnings and emitted as JsonElement or skipped, never silently mistyped: untagged unions, enveloped discriminated unions, cookie parameters, multipart bodies, status code ranges, array parameters other than explode: false string lists in the query, non-JSON request bodies other than bytes, non-JSON responses other than bytes and text/* string, response headers other than strings, integers and string literals. test/fixtures/unsupported.tsp holds one operation per shape and the snapshot test asserts each diagnostic; none of them is blocked by the design, they are simply not needed yet. Validation covers the top-level request body only.

Typed ids

The emitter's decorators live in its own TypeSpec library; other emitters ignore them. Tag a uuid-format string scalar once:

import "@olivervea/typespec-csharp";

using Olve.CSharp;

@format("uuid")
scalar Uuid extends string;

@id(Character) scalar CharacterId extends Uuid; // Id<Character>
@id(Place, "MyApp.Persistence.PlaceRecord") scalar PlaceId extends Uuid; // Id<MyApp.Persistence.PlaceRecord>
  • Every property and path/query parameter of CharacterId emits as Olve.Utilities.Ids.Id<Character>, ? and arrays included. The app references Olve.Utilities.
  • @id can also tag one property or parameter. To keep the main spec free of C# hints, apply it from a separate file: @@id(CharacterId, Character);.
  • The wire is unchanged: a uuid string. @id doesn't imply the format, so keep @format("uuid") (on a base scalar, as above) for OpenAPI and other clients. A value that isn't a uuid is a binding failure.
  • @id on anything but a uuid-format string is an error (id-not-uuid): Id<T> is backed by a Guid. explode: false query lists of ids are reported as unsupported.

Conformance

The emitter is proven once, in this repository: test/emitter.test.js snapshots the output of test/fixtures/widgets.tsp, and tests/Olve.TypeSpec.Conformance (.NET) compiles that output with the runtime, hosts it with trivial handlers and checks every operation and response over HTTP against the fixture's OpenAPI document; tests/Olve.TypeSpec.AotSmoke publishes the same app with Native AOT and checks a few requests against the binary. Services built on it test only their own behaviour.

npm test                                   # snapshots; UPDATE_SNAPSHOTS=1 npm test to accept new output
dotnet test --project tests/Olve.TypeSpec.Conformance   # from the repo root
dotnet publish tests/Olve.TypeSpec.AotSmoke -c Release -r linux-x64 -o artifacts/aot-smoke/publish && artifacts/aot-smoke/publish/Olve.TypeSpec.AotSmoke