Skip to content

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

Draft
Copilot wants to merge 18 commits intomainfrom
copilot/expand-back-compat-support
Draft

Expand back-compat property type preservation to all public model properties#10413
Copilot wants to merge 18 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
  • Invoke BuildPropertiesForBackCompatibility from ProcessTypeForBackCompatibility so it runs after all visitors
  • Update tests to call ProcessTypeForBackCompatibility to trigger property back-compat
  • Add tests covering nullable scalar, nullable enum, and top-level type-name change cases
  • Update backward-compatibility.md Model Properties section
  • Sync with latest main

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: 666f586

Comment thread packages/http-client-csharp/generator/docs/backward-compatibility.md Outdated
Comment thread packages/http-client-csharp/generator/docs/backward-compatibility.md Outdated
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>
Comment thread packages/http-client-csharp/generator/docs/backward-compatibility.md Outdated
…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>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 24, 2026

@copilot pull latest main and address merge conflicts

Synced with main in d323daf. Resolved conflict in ModelProvider.cs by keeping the version that routes properties through the BuildPropertiesForBackCompatibility hook (main's inline collection-only block was removed since the hook now covers all properties unconditionally). Renamed BackCompatibilityChangeCategory.CollectionPropertyTypePreservedPropertyTypePreserved since the label no longer accurately reflects the scope, and wired the category into the hook's Emitter.Info call. Build + 111 targeted tests pass.

@copilot there's another regression https://github.com/Azure/azure-sdk-for-net/pull/58604/changes#diff-e47645865a934988da49dabececfc8e6e5770d384b4afd15a5079ae0e9be3970R21. For readonly...

Partly related but not quite the same. The cascade in 2c0f1f7 keeps property.AsParameter.Type in sync with the overridden property type, so once back-compat decides the property should be ReadOnlyMemory<T>, the internal ctor param cascades to ReadOnlyMemory<T> too.

My hypothesis for this regression: the previous shipped binary had the public property as IReadOnlyList<T> (pre-ROM-visitor era), while the new spec + Azure ROM visitor produces ReadOnlyMemory<T>. Because back-compat now runs after all visitors, my pass sees current=ROM<T>, contract=IReadOnlyList<T>, different, so it overrides back to IReadOnlyList<T> and cascades that onto the ctor param — which is what you're seeing (public + internal ctor param flipped from ROM to a list).

Could you confirm the exact file/property (the diff link gives me a hash but not a path) and whether the previously-published public property on that type was IReadOnlyList<T> or ReadOnlyMemory<T>? If my hypothesis is right, the right behavior is debatable — preserving IReadOnlyList<T> is correct per the "always preserve prior public surface" rule Josh asked for, even though it undoes the ROM visitor's improvement. If you'd rather the ROM visitor win over back-compat for this specific shape, I can add a carve-out (skip override when the only difference is IReadOnlyList<T>ReadOnlyMemory<T> with the same element type).

@microsoft-github-policy-service microsoft-github-policy-service Bot added meta:website TypeSpec.io updates emitter:client:java Issue for the Java client emitter: @typespec/http-client-java emitter:client:python Issue for the Python client emitter: @typespec/http-client-python eng labels Apr 24, 2026
Comment thread .chronus/changes/bundler-support-sub-exports-2026-3-22-18-8-48.md
…ompat-support

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

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

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 24, 2026

@copilot an example of the ROM regression is https://github.com/Azure/azure-sdk-for-net/blob/295d15504609244e1dc78bc5c482c8d8e3818ea6/sdk/search/Azure.Search.Documents/src/Generated/Models/VectorizedQ...

Thanks for the concrete example — that changes my earlier hypothesis. VectorizedQuery.Vector is public ReadOnlyMemory<float> (matches prior binary) and internal ctor param is IList<float>. Since property.Type already equals the contract type, my back-compat pass never fires the override or the cascade here — so this regression isn't introduced by my changes per se, but it is exposed by them.

Root cause: the Azure ROM visitor sets property.Type = ReadOnlyMemory<T> but does not update the cached property.AsParameter instance that the serialization ctor uses. So the ctor param stays at the spec-derived type (IList<float>). Before this PR this mismatch existed too, but the shipped binary coincidentally had the same mismatched internal ctor, so nothing looked broken in diffs.

Two options to fix it here:

  1. Extend the cascade to run unconditionally on every property at the end of BuildPropertiesForBackCompatibility (cheap, broad — but will clobber any visitor that intentionally set AsParameter.Type to differ from property.Type, e.g. public ctor param IEnumerable<float> vs property ROM<float> — though that one goes through ToPublicInputParameter so it should be safe).
  2. Keep the cascade scoped as-is and fix the ROM visitor in Azure.Generator to also update AsParameter when it rewrites property.Type.

(1) is the right long-term fix since any current/future visitor that mutates property.Type has the same latent hazard. I'll go with that unless you prefer (2). Want me to push (1)?

@jorgerangel-msft
Copy link
Copy Markdown
Contributor

jorgerangel-msft commented Apr 24, 2026

@copilot

Before this PR this mismatch existed too, but the shipped binary coincidentally had the same mismatched internal ctor, so nothing looked broken in diffs.

This is not true. Before this PR, the Vector property was ROM and the internal ctor param was also ROM

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 24, 2026

@copilot

Before this PR this mismatch existed too, but the shipped binary coincidentally had the same mismatched internal ctor, so nothing looked broken in diffs.
...

You're right, I was wrong about that — apologies. If the prior binary had both public Vector and the internal ctor param as ReadOnlyMemory<float>, then this is a real regression introduced by this PR, and the move in 0f006de (back-compat pass → ProcessTypeForBackCompatibility, after visitors) is the likely culprit rather than the cascade.

My updated theory: the ROM visitor rewrites property.Type to ReadOnlyMemory<T> and relies on the serialization ctor being rebuilt afterwards (so its param picks up ROM via AsParameter). Previously, my back-compat pass ran inside BuildProperties before ctors/AsParameter were materialized, so the ordering worked out. Now back-compat runs after visitors, and by that time AsParameter has already been materialized with whatever property.Type was when the initial constructors were built — which for ROM properties is the pre-visitor wire type (IList<float>), and that's what the internal ctor param is stuck on.

If that's right, the fix I proposed as option (1) — unconditionally cascade property.Type onto AsParameter at the end of BuildPropertiesForBackCompatibility for every property, not just overridden ones — should resolve the ROM case too, because by that point the ROM visitor's property.Type = ROM<T> will have been applied and the cascade will sync AsParameter to match. Going to push that and then you can retry the regen to verify both the IList→IReadOnlyList case and the ROM case come out right.

@copilot an example of the ROM regression is https://github.com/Azure/azure-sdk-for-net/blob/295d15504609244e1dc78bc5c482c8d8e3818ea6/sdk/search/Azure.Search.Documents/src/Generated/Models/VectorizedQ...

Pushed option (1) in f671e08 — the cascade from property.TypeAsParameter/ToPublicInputParameter() now runs unconditionally for every property at the end of BuildPropertiesForBackCompatibility, so visitor-driven type changes like the Azure ROM rewrite stay in sync with the ctor params even when the override doesn't fire. All 1437 generator + 1312 ClientModel tests pass. Could you rerun the regen to confirm VectorizedQuery.Vector and similar ROM cases come out with the internal ctor param back to ReadOnlyMemory<T>?

…t pass

Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/feffcf68-840b-4dd5-8361-53035eddf36b

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
…e helper

Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/b7e2745a-474f-4b63-876f-d8cb936feb70

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

Copilot AI commented Apr 24, 2026

@copilot there's still a handful of issues in the latest regen:

Looked at all 4:

  1. r3139793313 (PersistentThreadMessage.Attachments) and r3139793331 (VectorizedQuery ctor body with vector.ToList()/AssertNotNull on ROM): both are ctor body statements that are stale relative to the parameter type. The unconditional cascade I pushed in f671e08 updates AsParameter.Type but does not re-emit ctor body statements, so leftover expressions like vector.ToList() / attachments?.ToList() remain from when the param was a list. I think f671e08 is the wrong approach and should be reverted — the cascade should only fire when back-compat actually overrides property.Type (i.e., the 2c0f1f7 scope). That means the ROM visitor scenario (VectorizedQuery.Vector) goes back to being a pre-existing latent issue and should be fixed in the ROM visitor itself (option 2 from my earlier message). Want me to go that route?
  2. r3139793391 (AgentsPersistentModelFactory.metadata: IReadOnlyDictionary→IDictionary): likely caused by the same cascade — the back-compat-preserved property type (IDictionary) is pushed through ToPublicInputParameter() onto the factory parameter. Factories should keep the reader-friendly IReadOnlyDictionary input; reverting f671e08 should fix this too.
  3. r3139793486 (maxPageSizemaxpagesize): looks unrelated to property-type preservation — this is a parameter-name casing change. Was this present in the prior regen too? I don't see a code path in this PR that would rename parameters.

Proposed next step: revert f671e08 (unconditional cascade) and keep 2c0f1f7 (override-only cascade). That addresses #1, #2, #4 as a set. #3 I'd like to confirm is pre-existing before acting. LMK if you want me to push the revert.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/repos/Azure/azure-sdk-for-net/pulls/comments/3139793313
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js (http block)
    • Triggering command: /usr/bin/curl curl -sSL REDACTED (http block)
  • https://api.github.com/repos/Azure/azure-sdk-for-net/pulls/comments/3139793331
    • Triggering command: /usr/bin/curl curl -sSL REDACTED (http block)
  • https://api.github.com/repos/Azure/azure-sdk-for-net/pulls/comments/3139793391
    • Triggering command: /usr/bin/curl curl -sSL REDACTED (http block)
  • https://api.github.com/repos/Azure/azure-sdk-for-net/pulls/comments/3139793486
    • Triggering command: /usr/bin/curl curl -sSL REDACTED (http block)

If you need me to access, download, or install something from one of these locations, you can either:

@jorgerangel-msft
Copy link
Copy Markdown
Contributor

@copilot lets revert the unconditional cascade but also fix the stale _inputParameter field in ParameterProvider. When updating the parameter, lets call Update instead of using the property setter. If a parameter is updated via the Update method, then we should reset the _inputParameter instance to be null to allow the input parameter to be recalculated on access.

…meterProvider.Update

Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/e4177a1f-cc04-4b19-b625-12c5c0180923

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

Copilot AI commented Apr 24, 2026

@copilot lets revert the unconditional cascade but also fix the stale _inputParameter field in ParameterProvider. When updating the parameter, lets call Update instead of using t...

Done in 3df9c80. Scoped SyncCachedParametersWithPropertyType back to the override branch only and added _inputParameter = null at the top of ParameterProvider.Update so ToPublicInputParameter() recalculates from the updated state on next access. The sync helper no longer manually touches the public input variant — it just calls Update(type: ...) on the parameter. All 1437 generator + 1312 ClientModel 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 emitter:client:java Issue for the Java client emitter: @typespec/http-client-java emitter:client:python Issue for the Python client emitter: @typespec/http-client-python eng meta:website TypeSpec.io updates ui:playground

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expand Property Back Compat Support for All Model Properties

3 participants