Skip to content

[http-client-csharp] Fix external model serialization ownership and factory discriminators - #12343

Merged
JoshLove-msft merged 6 commits into
microsoft:mainfrom
JoshLove-msft:joshlove-msft-external-model-serialization
Oct 23, 2026
Merged

JoshLove-msft merged 6 commits into
microsoft:mainfrom
JoshLove-msft:joshlove-msft-external-model-serialization

Conversation

@JoshLove-msft

@JoshLove-msft JoshLove-msft commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Allow an explicit [CodeGenMember] property on the first generated subclass to replace a TypeSpec property added to an externally mapped base.
  • Preserve its inherited serialization-constructor slot and metadata instead of introducing a second deserialization branch/constructor argument.
  • Give the explicit replacement ownership of its JSON key, excluding that key from generated raw and typed additional-data writers while preserving unrelated data and existing read-only/wire-format rules.
  • Recover discriminator metadata for matching handwritten serialization-constructor parameters. Known-variant factories keep their fixed discriminator; unknown variants remain flexible.

Root causes and supported scope

The canonical customization view only considered locally declared TypeSpec properties. A proxy for an inherited external-base property therefore became a separate member, while the external writer could not serialize the newly added property. A proxy backed by additional data could also write the same JSON key twice.

Handwritten constructor parameters do not carry generated PropertyProvider metadata. Additionally, an external extensible discriminator struct's runtime/symbol representations differ in generator-only enum annotations, so strict CSharpType.Equals cannot establish CLR identity. Factory matching now uses the original parameter name, namespace, CLR type-name identity and parameter modifiers before restoring discriminator metadata.

This does not invent external CLR members, constructors, additional-data storage or patch behavior. Consumers must explicitly bind extension properties and supply constructors that call the actual accessible external constructor when its shape differs. Wire-name aliases alone are not silently deduplicated, and properties already owned by the external writer must not be claimed again.

Validation

  • Added regressions and observed failures before the corresponding fixes, including actual duplicate JSON keys and the external extensible-struct factory gap.
  • Complete emitted models, serialization partials, factories, helpers and MRW-generated context compile and execute. Eight runtime combinations cover JSON/wire formats, raw/typed bags and read-only properties, with unique-key and read/write/read assertions through base JSON and persistable interfaces. Negative alias cases and compiled known/unknown factory cases are included.
  • Core generator: 2,944 passed; final factory rerun: 86 passed.
  • ClientModel: 2,519 passed; Input: 304 passed.
  • Emitter: 384 passed, 2 skipped.
  • Full eng/scripts/Generate.ps1 regeneration succeeded with no generated-file changes.
  • Full generator solution build, emitter build and API extraction passed; repository formatting, package-scoped oxlint --type-aware --deny-warnings, and npm run cop passed.

Windows validation used short-path mappings and an artifact-only MSBuild target to avoid existing MAX_PATH limits when copying source fixtures. The package's stale npm run lint references unavailable ESLint; the repository-standard oxlint command above passed instead. No repository/toolchain configuration was changed for these workarounds.

The coordinating Azure SDK investigation also generated and compiled an actual OpenAI 2.14-derived fixture using these shared DLLs: both keys occur once in J/W through derived/base interfaces, values round-trip, and the concrete factory fixes its discriminator. All 315 Azure.Generator tests passed. The independent downstream guard is Azure/azure-sdk-for-net#64105, not a consuming generator release.

SDK integration

Consume the updated shared generator assemblies, bind proxies using [CodeGenMember("ResponseId")] / [CodeGenMember("AgentReference")], and regenerate. Retain the single original constructor argument for each property; remove duplicate proxy-specific arguments. Keep necessary custom constructor/storage logic; CodeGenSerialization.SerializationName can complement the binding but does not replace it.

--generated by Copilot

…ership

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@pkg-pr-new

pkg-pr-new Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/http-client-csharp@12343

commit: 02cee29

@github-actions

Copy link
Copy Markdown
Contributor

No changes needing a change description found.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Normalized-name bindings can lose serialization metadata, and required replacements can produce duplicate constructor parameters in generated grandchildren.

2 open issues
What changed in this PR

This PR updates the C# client generator so explicit custom properties can own serialization of properties added to externally mapped bases, while model factories preserve discriminator behavior.

Changes:

  • Bind external-base replacements to inherited metadata and constructor slots, and exclude their JSON keys from generated additional-data writers.
  • Recover discriminator metadata for handwritten constructors, keeping known variants fixed and unknown variants flexible.
  • Add compilation and round-trip regressions and document customization boundaries.
File Description
packages/​http-client-csharp/​generator/​Microsoft.TypeSpec.Generator/​test/​Providers/​ModelFactories/​TestData/​ModelFactoriesCustomizationTests/​CustomConstructorDiscriminator/​Models.cs Handwritten constructor fixture for discriminator tests.
packages/​http-client-csharp/​generator/​Microsoft.TypeSpec.Generator/​test/​Providers/​ModelFactories/​ModelFactoriesCustomizationTests.cs Compiled known/unknown discriminator factory tests.
packages/​http-client-csharp/​generator/​Microsoft.TypeSpec.Generator/​src/​Providers/​ModelProvider.cs Reuses inherited constructor slots for explicit replacements.
packages/​http-client-csharp/​generator/​Microsoft.TypeSpec.Generator/​src/​Providers/​ModelFactoryProvider.cs Restores handwritten discriminator parameter metadata.
packages/​http-client-csharp/​generator/​Microsoft.TypeSpec.Generator/​src/​Providers/​CanonicalTypeProvider.cs Discovers explicitly claimed external-base properties.
packages/​http-client-csharp/​generator/​Microsoft.TypeSpec.Generator.ClientModel/​test/​Providers/​MrwSerializationTypeDefinitions/​TestData/​SerializationCustomizationTests/​SerializationNameDoesNotReplaceInheritedProperty/​Model.cs Negative fixture for wire-name-only aliases.
packages/​http-client-csharp/​generator/​Microsoft.TypeSpec.Generator.ClientModel/​test/​Providers/​MrwSerializationTypeDefinitions/​TestData/​SerializationCustomizationTests/​ExternalPropertyReplacement/​Model.cs Explicit replacement backed by additional data.
packages/​http-client-csharp/​generator/​Microsoft.TypeSpec.Generator.ClientModel/​test/​Providers/​MrwSerializationTypeDefinitions/​SerializationCustomizationTests.cs Compilation, unique-key, and round-trip coverage.
packages/​http-client-csharp/​generator/​Microsoft.TypeSpec.Generator.ClientModel/​test/​ModelReaderWriterValidation/​TestProjects/​Sample_TypeSpec/​TestData/​ExternalModelPropertiesTests/​ExternalResponse.cs External model and discriminator fixtures.
packages/​http-client-csharp/​generator/​Microsoft.TypeSpec.Generator.ClientModel/​src/​Providers/​MrwSerializationTypeDefinition.cs Excludes replacement-owned keys from additional-data writers.
packages/​http-client-csharp/​.tspd/​docs/​customization.md Documents external-property ownership and constructor requirements.

Review effort: Balanced

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 22, 2026 16:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

External-property matching misses supported normalized names, and constructor substitution narrows public collection inputs.

2 open issues
2 previously missed issues

In code that hasn't changed since last review

Medium severity Support normalized names when matching external property bindings

packages/​http-client-csharp/​generator/​Microsoft.TypeSpec.Generator/​src/​Providers/​CanonicalTypeProvider.cs:63

This prefilter rejects normalized bindings such as [CodeGenMember("IPFoo")] for ipFoo and [CodeGenMember("CreatedOn")] for a date-time creationDate property, leaving the external-property proxy without inherited serialization metadata or constructor-slot replacement. Include the non-exact-name normalization already used by this class at lines 41–44 and covered by ModelCustomizationTests.CustomCodeReplacesAcronymNormalizedProperty and CustomCodeReplacesDateNormalizedProperty.

Medium severity Preserve IEnumerable parameters for initialization constructors

packages/​http-client-csharp/​generator/​Microsoft.TypeSpec.Generator/​src/​Providers/​ModelProvider.cs:1627

For a required list replacement, this substitutes IList<T> for the inherited initialization constructor's IEnumerable<T> parameter, so callers can no longer pass arbitrary enumerable sequences. Preserve ToPublicInputParameter() for initialization constructors, as AddInitializationParameterForCtor does at ModelProvider.cs:1858 and ModelProviderTests.ValidateListParameterHandlingInConstructor verifies; keep AsParameter for serialization constructors.

Review effort: Balanced

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 22, 2026 16:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved collection-constructor and collision-adjusted binding cases can produce uncompilable models or omit serialized properties.

2 open issues
2 resolved since last review

Review effort: Balanced

…puts

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 22, 2026 22:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Additional-properties writers on descendants can still duplicate JSON keys owned by inherited external-property replacements.

1 open issue
2 resolved since last review

Review effort: Balanced

…ship

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 22, 2026 23:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Shared constructor metadata and serialization ownership changes across external inheritance boundaries warrant final human compatibility review despite the extensive regressions.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

…tent

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 23, 2026 15:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Cross-assembly constructor matching and inherited serialization ownership warrant human confirmation of the reported runtime validation.

No open issues 🎉

Review effort: Balanced

@JoshLove-msft
JoshLove-msft added this pull request to the merge queue Oct 23, 2026
Merged via the queue into microsoft:main with commit f105adb Oct 23, 2026
30 checks passed
@JoshLove-msft
JoshLove-msft deleted the joshlove-msft-external-model-serialization branch October 23, 2026 17:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

emitter:client:csharp Issue for the C# client emitter: @typespec/http-client-csharp

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants