microsoft / microsoft/typespec

C# emitter: non-string-backed referenced extensible enums emit inaccessible internal ToSerial{Name}() on write

Open
#11,230 1 comment 1 reaction 0 assignees View on GitHub
bug emitter:client:csharp
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.