microsoft / microsoft/typespec
C# emitter: non-string-backed referenced extensible enums emit inaccessible internal ToSerial{Name}() on write
- Dominant language
- Java
- Stars
- 5.9k
- Forks
- 394
- Avg merge
- 1d 23h
- Merged PRs (30d)
- 104
Description
## Summary
Non-string-backed (int/long/float/double) **referenced** (external, cross-assembly) extensible enums serialize through an `internal` helper that is inaccessible from the consuming assembly, producing non-compiling code.
This was found while reviewing #11180, which restores enum semantics for referenced extensible enums so they serialize/deserialize inline instead of falling through the (runtime-throwing) `ModelReaderWriter.Read` path. That fix is fully correct for **string-backed** extensible enums (the common case, and all discriminators). This issue tracks the remaining **non-string** case, which #11180 intentionally leaves for a follow-up.
## Repro
A model with a property typed as an **int-backed external extensible enum** generates:
```csharp
// write
writer.WriteNumberValue(Kind.Value.ToSerialInt32());
// deserialize
kind = new global::System.Guid(prop.Value.GetInt32()); // (System.Guid used as the external stand-in in the test)
```
Deserialize is fine — it uses the enum's **public** constructor. The write path is the problem.
## Root cause
- The write path for an extensible (struct) enum with a non-string underlying type emits `value.ToSerial{UnderlyingEnumType.Name}()` (`MrwSerializationTypeDefinition.SerializeJsonValueCore`, and `CSharpTypeSnippets.ToSerial`).
- That `ToSerial{Name}()` helper is generated with **`Internal`** accessibility (`ExtensibleEnumSerializationProvider.BuildMethods`, `Modifiers: MethodSignatureModifiers.Internal`).
- The generated extensible-enum struct exposes **no public accessor that returns the raw numeric underlying value** — only a public `ToString()` (string), a public constructor, and a public inbound implicit operator (underlying → enum) (`ExtensibleEnumProvider.BuildMethods` / `BuildConstructors`).
So for a **referenced** non-string extensible enum, the consuming assembly emits `value.ToSerialInt32()` against an `internal` member in the other assembly → **CS0122** (there is no `InternalsVisibleTo` between independently generated packages).
For the string case there is no such gap: serialize uses the public `ToString()` and deserialize uses the public constructor, both accessible cross-assembly.
## Impact
Niche: non-string extensible enums are rare, and referenced (cross-package) ones rarer still. Before #11180 this scenario failed at runtime (the `ModelReaderWriter.Read` throw); after #11180 (if not scoped) it would fail at compile time. #11180 scopes the enum-semantics restoration to string-backed enums, so this scenario retains its prior (pre-#11180) behavior until this issue is addressed.
## Proposed fix options
1. **Expose the underlying value publicly** for non-string extensible enums — e.g. make `ToSerial{Name}()` public, or add a public explicit operator to the underlying type. This mirrors the already-public inbound constructor/implicit operator and makes cross-assembly serialization work. It is a public-API-surface addition, so it needs API-review sign-off.
2. Keep the current scoping and, once (1) lands, extend the enum-semantics restoration in `TypeFactory.CreateExternalType` to non-string underlying types as well.
## Acceptance
- A referenced int-backed extensible enum used as a model property (and as a polymorphic discriminator) round-trips through JSON serialization with code that compiles across assemblies.
- Unit tests covering the write path for a non-string referenced extensible enum.
Contributor guide
Research direction
Start in MrwSerializationTypeDefinition.SerializeJsonValueCore and CSharpTypeSnippets.ToSerial, then inspect ExtensibleEnumSerializationProvider.BuildMethods and ExtensibleEnumProvider.BuildMethods/BuildConstructors. Run or add unit coverage for a referenced int-backed extensible enum used as a model property and polymorphic discriminator. Done means the generated cross-assembly code compiles and round-trips through JSON serialization.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100