Skip to content

Build models, properties and enums on the emitter framework - #11601

Closed
Timothee Guerin (timotheeguerin) wants to merge 3 commits into
microsoft:mainfrom
timotheeguerin:hsc/models-on-emitter-framework
Closed

Timothee Guerin (timotheeguerin) wants to merge 3 commits into
microsoft:mainfrom
timotheeguerin:hsc/models-on-emitter-framework

Conversation

@timotheeguerin

@timotheeguerin Timothee Guerin (timotheeguerin) commented Aug 7, 2026 •

Copy link
Copy Markdown
Member

The emitter shipped private forks of the framework's ClassDeclaration, Property and EnumDeclaration — copies made because the framework offered no way to adjust them, which then drifted. With declaration overrides and component props now available (#11597, #11598), the forks go away and the divergences are expressed as configuration:

  • The property policy (nullability, virtual/override/new, error-model members, JSON attributes) becomes a registered ModelProperty declaration override.
  • ServerClassDeclaration becomes EfClassDeclaration with an explicit properties list and the generated constructors passed as children.
  • Enums renders through EfEnumDeclaration with an explicit members list, keeping the emitter's more permissive union-as-enum detection.

models.tsx drops from ~270 lines to ~125, and the whole change is a net -350 lines.

One structural note for reviewers: the framework's TypeExpression recurses into itself, not into a consumer's wrapper. Any type kind whose handling recurses into a contained type therefore has to stay in the emitter's wrapper or its divergences are silently lost for the nested type. Scalars are the exception — they go through component overrides, which apply at every level. This is now written down in a comment at the top of the wrapper, because it is not obvious and it caused a regression while writing this.

Generated output is unchanged.


This is the tip of the series and is still blocked on #11598 and #11600, whose commits it currently carries. A PR can only declare one base, so this stays on main and will reduce to a single commit as they merge.

One of 7 PRs moving @typespec/http-server-csharp onto the emitter framework instead of its private forks. Merged: #11596, #11597, #11599. Remaining: #11598, #11600, #11601, #11602.

@pkg-pr-new

pkg-pr-new Bot commented Aug 7, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/emitter-framework@11601
npm i https://pkg.pr.new/@typespec/http-server-csharp@11601

commit: 33e11c2

@github-actions

github-actions Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

All changed packages have been documented.

  • ✅ @typespec/emitter-framework
  • ✅ @typespec/http-server-csharp
Show changes

@typespec/emitter-framework - feature ✏️

Let emitters author the C# declaration components instead of forking them,> ,> - ClassDeclaration accepts an explicit properties list and extra members as children.,> - Property accepts every Alloy property prop, plus name and csharpType overrides.,> - EnumDeclaration accepts an explicit members list and a jsonAttributes prop.,> - JsonConverter accepts doc, access modifiers, extra members, an explicit csharpType, and a readReturns override.,> ,> tsx,> <ClassDeclaration type={model} properties={model.properties.values().filter(isVisible)} partial>,> <Constructor />,> </ClassDeclaration>,>

@typespec/http-server-csharp - fix ✏️

Fix generated error model constructors and numeric constraint attributes using the wrong C# types,> ,> Error model constructors declared parameters such as DateOnly and Uri while the matching properties were DateTime and string, producing code that did not compile. NumericConstraintAttribute<T> had the same mismatch, which stopped the converter from binding.

@typespec/http-server-csharp - internal ✏️

Build model, property and enum generation on @typespec/emitter-framework components,> ,> The emitter carried private forks of the framework's ClassDeclaration, Property and EnumDeclaration. They are now consumed directly, with the emitter's own behavior expressed as a declaration override and component props. Generated output is unchanged.

@azure-sdk-automation

azure-sdk-automation Bot commented Aug 7, 2026 •

Copy link
Copy Markdown

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

ClassDeclaration takes an explicit property list and extra members, Property
takes the full Alloy property prop set plus name/csharpType overrides,
EnumDeclaration takes an explicit member list and jsonAttributes, and
JsonConverter takes doc, access modifiers, extra members and an explicit
csharpType.
…ource of truth

The emitter had three disagreeing scalar-to-C# name maps, so error model
constructors declared parameters such as DateOnly, Uri and sbyte while the
matching properties were DateTime, string and SByte - code that does not
compile. NumericConstraintAttribute<T> had the same mismatch.
…he emitter framework

Replaces the private forks of ClassDeclaration, Property and EnumDeclaration with
the framework components, expressing the emitter's own behavior as a declaration
override and component props. Generated output is unchanged.
Timothee Guerin (timotheeguerin) added a commit to timotheeguerin/typespec that referenced this pull request Aug 28, 2026
…crosoft#11597)

An override descriptor already accepted a `declaration` entry, but
nothing ever dispatched to it — only `reference` was wired up. So an
emitter could change how a type is *referenced* and had no say in how it
is *declared*. The only way to customize a declaration was to copy the
framework component into your own package and diverge from it, which is
exactly what `@typespec/http-server-csharp` did for classes, properties
and enums.

`Experimental_OverridableComponent` now dispatches `declaration` too,
and the C# `ClassDeclaration`, `Property` and `EnumDeclaration` render
through it. An override can replace the declaration outright, or
re-render the default with different props:

```tsx
const overrides = Experimental_ComponentOverridesConfig().forTypeKind("ModelProperty", {
  declaration: (props) =>
    isSecret(props.type) ? (
      <props.Declaration {...props.declarationProps} public={false} internal />
    ) : (
      props.default
    ),
});
```

`props.Declaration` is the unwrapped body, so re-rendering it does not
recurse back through the override.

The declaration props type is generic and defaults to `Record<string,
any>`, because the existing `Experimental_CustomTypeToProps` map is
TypeScript-specific (`VarDeclarationProps`, `ObjectPropertyProps`) and
means nothing for C#. Callers that want type safety pass the concrete
props type: `.forTypeKind<"ModelProperty", PropertyProps>(...)`.

---

Independent — targets `main` and can merge on its own.

One of 7 PRs moving `@typespec/http-server-csharp` onto the emitter
framework instead of its private forks: microsoft#11596, microsoft#11597, microsoft#11598, microsoft#11599,
microsoft#11600, microsoft#11601, microsoft#11602.
Timothee Guerin (timotheeguerin) added a commit to timotheeguerin/typespec that referenced this pull request Aug 28, 2026
The emitter carried a 217-line hand-maintained table of C# keywords and
a `createLibrary` call re-declaring the `System.Text.Json.Serialization`
attributes. `@alloy-js/csharp` ships both, and its versions are better:
the local keyword list was missing `delegate`, and hand-declared symbols
do not participate in Alloy's automatic `using` management.

Both are deleted in favour of the Alloy equivalents
(`isValidCSharpIdentifier`, `csharpKeywords`, the `System/Text/Json`
builtins), along with the local `getDocComments` copy, which is already
exported from `@typespec/emitter-framework/csharp`.

One thing Alloy deliberately does *not* do is worth calling out: its
name policy `@`-escapes real keywords, but the emitter needs namespace
segments colliding with common BCL type names to be **renamed** — a
namespace called `Type` shadows `System.Type` and breaks every
`typeof()` in the generated converters. That rule survives, now isolated
in `getCSharpNamespaceName` and built on Alloy's keyword sets rather
than a parallel list.

Generated output is unchanged.

---

Independent — targets `main` and can merge on its own.

One of 7 PRs moving `@typespec/http-server-csharp` onto the emitter
framework instead of its private forks: microsoft#11596, microsoft#11597, microsoft#11598, microsoft#11599,
microsoft#11600, microsoft#11601, microsoft#11602.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Timothee Guerin (timotheeguerin) added a commit to timotheeguerin/typespec that referenced this pull request Aug 28, 2026
The C# `TypeExpression` threw a hard `Error` for anything it did not
recognise — tuples, string templates, enum members, template parameters
and most of the intrinsics. An emitter hitting one of those got a crash
with no source location instead of a diagnostic, so
`@typespec/http-server-csharp` had to wrap every call site in
`try`/`catch`.

Those kinds are now handled, and a genuinely unsupported type reports a
diagnostic against the offending type and falls back to `object`, so
compilation continues and the user gets a pointer to the spec that
caused it. The diagnostics name the type, so it is actionable without
bisecting the spec:

```
warning emitter-framework/csharp-unsupported-scalar: Scalar 'MyLib.ipAddress' has no
  C# equivalent, using 'object' instead. Extend a built-in scalar to control how it is emitted.

warning emitter-framework/csharp-unsupported-type: Type '@MyLib.flag' of kind 'Decorator'
  is not supported in C#, using 'object' instead.
```

The two are kept separate because they call for different actions: the
scalar one is fixable in the spec by extending a built-in scalar, while
the other reports a gap in the emitter.

Two smaller bugs fixed along the way:

- Unsupported scalars reported a **TypeScript** diagnostic
(`typescript-unsupported-scalar`) from the C# components. C# now has its
own `src/csharp/lib.ts`, matching the existing `typescript/lib.ts` and
`python/lib.ts`, which makes that class of mistake a type error.
- The `null` and `never` intrinsics emitted `null` and `never`, neither
of which is a C# type. They now emit `object` and `void`.

Also adds an `isCSharpValueType` util, since deciding whether a TypeSpec
type maps to a C# struct is something every C# emitter needs and every
C# emitter was reimplementing.

---

Independent — targets `main` and can merge on its own.

One of 7 PRs moving `@typespec/http-server-csharp` onto the emitter
framework instead of its private forks: microsoft#11596, microsoft#11597, microsoft#11598, microsoft#11599,
microsoft#11600, microsoft#11601, microsoft#11602.
@microsoft-github-policy-service

Copy link
Copy Markdown
Contributor

Hi @Timothee Guerin (@timotheeguerin). The PR will be closed since the PR has no update for 28 days. If this is still relevant please reopen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

emitter-framework Issues for the emitter framework stale Mark a PR that hasn't been recently updated and will be closed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant