Skip to content

Expand back-compat property type preservation to all public model properties#10413

Open
Copilot wants to merge 6 commits intomainfrom
copilot/expand-back-compat-support
Open

Expand back-compat property type preservation to all public model properties#10413
Copilot wants to merge 6 commits intomainfrom
copilot/expand-back-compat-support

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 17, 2026

  • Add BuildPropertiesForBackCompatibility virtual hook on TypeProvider
  • Move ModelProvider back-compat property logic into the new hook
  • Always preserve the last contract's property type when it differs from the current spec — for any property kind (collections, scalars, enums, models) and any kind of change (collection wrapper, nullability, underlying type). Users can override with custom code if they want the new spec's type.
  • Add tests covering nullable scalar, nullable enum, and top-level type-name change cases
  • Update backward-compatibility.md Model Properties section to describe the unconditional preservation behavior
  • Sync with latest main and resolve merge conflicts in ModelProvider.cs

Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/33d000a2-d811-4f6c-a72b-359b57cee1a2

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
@microsoft-github-policy-service microsoft-github-policy-service bot added the emitter:client:csharp Issue for the C# client emitter: @typespec/http-client-csharp label Apr 17, 2026
Copilot AI changed the title [WIP] Expand property back compat support for all model properties Expand back-compat property type preservation to all public model properties Apr 17, 2026
Copilot AI requested a review from jorgerangel-msft April 17, 2026 18:09
@JoshLove-msft JoshLove-msft marked this pull request as ready for review April 17, 2026 21:37
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 17, 2026

Open in StackBlitz

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

commit: 86c40d0


- **Collection wrapper change:** A property previously generated as a read-only collection (e.g. `IReadOnlyList<T>` / `IReadOnlyDictionary<TKey, TValue>`) that is now produced as a read-write collection (e.g. `IList<T>` / `IDictionary<TKey, TValue>`), or vice versa. The collection element/key/value type names must still match – the wrapper is preserved but genuine element-type changes are honoured.
- **Nullability change:** A scalar, enum, or model property that was previously generated as nullable (e.g. `int?`, `StatusEnum?`) and is now produced as non-nullable (or vice versa). The last contract's nullability is preserved when the top-level type name (and any generic argument names) still matches.
- **Same-name types from different sources:** Properties whose generated type is logically the same as the last contract's type, but sourced from different assemblies (e.g. a `TypeProvider`-produced type vs. a compiled-assembly type). Equality is evaluated by name rather than identity so these are treated as the same type.
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.

@copilot remove references to TypeProvider. That's an implementation detail.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed in fd9ef6e — the Model Properties section no longer references TypeProvider.

- The generator compares property types against the `LastContractView`
- For read-write lists and dictionaries, if the previous type was different, the previous type is retained
- The generator compares property types against the `LastContractView`.
- For read-write lists and dictionaries, if the previous type was different but the element/key/value type names match, the previous type is retained.
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.

@copilot this is no longer the case. Pull the latest main and remove mention of this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Pulled the latest backward-compatibility.md from main and rewrote the Implementation Details to describe the unified "same top-level name → retain previous type" rule, dropping the collection-only wording. Done in fd9ef6e.

// Backcompat fix for property types: if a property existed in the last contract
// with a compatible but non-identical type, retain the previous type to avoid
// breaking consumers of the library.
if (TryGetLastContractPropertyTypeOverride(outputProperty, out var lastContractPropertyType))
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.

@copilot Since we are now handling all properties, we should extend our back compat APIs in TypeProvider to have a new BuildPropertiesForBackCompatibility method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added protected internal virtual BuildPropertiesForBackCompatibility(IEnumerable<PropertyProvider>) to TypeProvider in fd9ef6e, matching the shape of BuildMethodsForBackCompatibility / BuildConstructorsForBackCompatibility. ModelProvider now overrides it to perform the type-override pass, and BuildProperties routes its output through the new hook so every property gets back-compat treatment. I kept the call site inside BuildProperties (rather than ProcessTypeForBackCompatibility) so the property type is settled before serializers/writers or visitors read Properties — moving it to ProcessTypeForBackCompatibility broke several existing tests that rely on the type being present at property-build time. Happy to revisit if you'd prefer a different trigger point.

Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/e014a173-0713-44e4-a0b5-4ce8f896d407

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
- The generator compares property types against the `LastContractView`
- For read-write lists and dictionaries, if the previous type was different, the previous type is retained
- The generator compares property types against the `LastContractView`.
- If the previous type is logically compatible with the new type (same top-level name, with matching generic argument names), the previous type is retained. This covers read-write collection wrapper changes (e.g. `IList<T>` ↔ `IReadOnlyList<T>`) as well as nullability changes on scalars, enums, and models.
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.

@copilot this is no longer the case - sync from main

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Could you clarify what you'd like changed here? Line 168 describes the unified rule introduced by this PR (collection wrapper changes + nullability), and main hasn't moved since I last synced it (ab5caa4 is still tip for this file). If you'd prefer I drop the "Implementation Details" bullet list entirely — keeping just the two scenario sections above — happy to do that.

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.

main has moved since you last synced @copilot

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right, sorry — I missed 38a8c7a (the revert of #10319). Merged main in 6275b90 and dropped the AreNamesEqual generic-argument check from the collection branch so it matches main's behavior (override on any inequality). Updated the doc accordingly. Non-collection properties still use the conservative top-level-name-match rule for the new nullability case.

…n back-compat)

Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/1cb571b4-94fe-450a-bda0-73a4c98e8f9f

Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com>
@JoshLove-msft
Copy link
Copy Markdown
Contributor

@copilot sync from main and resolve all merge issues

…ompat-support

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

Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 18, 2026

@copilot sync from main and resolve all merge issues

Synced from main in 7c64485. Resolved the conflict in ModelProvider.cs (kept the version that routes properties through the new BuildPropertiesForBackCompatibility hook instead of the inline back-compat block). All 80 ModelProviderTests still pass.

// argument names) to match. This ensures we only override when the types are
// logically the same (e.g. differ only in nullability) and never when the
// underlying type has genuinely changed (e.g. string to int).
if (outputProperty.Type.AreNamesEqual(candidate))
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.

@copilot why wouldn't we just always enforce the existing type in the back compat scenario? Users can work around this with custom code if they need to.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point — done in 86c40d0. TryGetLastContractPropertyTypeOverride now always returns the last contract's type whenever it differs from the new spec, with no collection-vs-scalar or AreNamesEqual branching. Updated the previously-negative test (BackCompat_ScalarPropertyTypeOverriddenWhenTypeNameDiffers) to assert the override now applies even when the top-level name changes (stringint is preserved as string), and rewrote the docs to describe the unconditional behavior. All 1317 generator tests pass.

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.

Expand Property Back Compat Support for All Model Properties

3 participants