Skip to content

fix(http-client-csharp): resolve System.ClientModel types as framework types - #11678

Merged
JoshLove-msft merged 1 commit into
microsoft:mainfrom
JoshLove-msft:fix/11676-filebinarycontent-framework-type
Aug 14, 2026
Merged

fix(http-client-csharp): resolve System.ClientModel types as framework types#11678
JoshLove-msft merged 1 commit into
microsoft:mainfrom
JoshLove-msft:fix/11676-filebinarycontent-framework-type

Conversation

@JoshLove-msft

Copy link
Copy Markdown
Contributor

Fixes #11676

Root cause

Not PR #11585 (the auto-triage hypothesis in the issue). The real cause is framework-type resolution.

TypeFactory.CreateFrameworkType ultimately falls back to Type.GetType(fullyQualifiedTypeName). An unqualified Type.GetType only probes corlib and the assembly that declares the calling method — here Microsoft.TypeSpec.Generator.dll, which does not reference System.ClientModel. So "System.ClientModel.FileBinaryContent" always resolved to null. (This is the same reason System.BinaryData, System.Uri, System.Text.Json.JsonElement and System.Net.IPAddress already needed hard-coded special cases there.)

Verified that this is not fixable by moving the call or by adding metadata references:

compile-time reference resolves: System.ClientModel.FileBinaryContent
assembly loaded: System.ClientModel
Type.GetType from referencing assembly: <null>
Assembly.GetType on SCM assembly: System.ClientModel.FileBinaryContent

Even from an assembly that directly references and has loaded System.ClientModel, Type.GetType returns null. CodeModelGenerator.AdditionalMetadataReferences does not help either — that is a Roslyn/symbol-binding concern and has no effect on CLR reflection.

How that produced the reported symptoms

  1. TypeSymbolExtensions.GetCSharpType calls CreateFrameworkType, gets null, and builds a symbol-backed CSharpType { Name = "FileBinaryContent", Namespace = "System.ClientModel", IsFrameworkType = false }.
  2. ModelProvider.BuildProperties back-compat handling sees !lastContractPropertyType.Equals(outputProperty.Type) (framework-backed vs symbol-backed), concludes the contract changed, and overwrites the property type with the symbol-backed look-alike.
  3. ScmModel.IsFileBinaryContentType then returns false, so:
    • BuildMultipartFileConstructors returns null → the string / Stream / BinaryData convenience constructors disappear
    • the [Experimental("SCME0004")] attributes on the constructor, property and model-factory method are dropped, along with using System.Diagnostics.CodeAnalysis;
    • MultipartFormDataSerializationDefinition.BuildScalarAdd picks the model Add<T> overload instead of the FileBinaryContent one

Why it looked intermittent

The bad path only runs when a last contract is actually resolved (SourceInputModel.FindForTypeInLastContract). Runs without a last-contract assembly available never hit it, which is why byte-identical inputs produced different output across CI runs. The dotnet msbuild version delta noted in the issue is a proxy for that, not the cause.

Fix

Override CreateFrameworkType in ScmTypeFactory and fall back to Assembly.GetType scoped to the System.ClientModel assembly:

protected override Type? CreateFrameworkType(string fullyQualifiedTypeName)
    => base.CreateFrameworkType(fullyQualifiedTypeName)
        ?? typeof(BinaryContent).Assembly.GetType(fullyQualifiedTypeName);

This is deterministic (the assembly is a compile-time reference of Microsoft.TypeSpec.Generator.ClientModel) and fixes every System.ClientModel type, not just FileBinaryContent. Deliberately avoided broad probing such as AppDomain.CurrentDomain.GetAssemblies(), which would reintroduce load-order nondeterminism — exactly the class of problem this issue is about.

Tests

Added TestMultipartFormDataModel_LastContractFileType_KeepsFileBinaryContentFrameworkType, which loads a last contract declaring public FileBinaryContent ProfileImage { get; } and asserts the property stays a framework type, that IsFileBinaryContentType recognizes it, and that the emitted model is byte-identical to the no-last-contract output. It fails on main with Expected: True But was: False and passes with this change.

Validation

  • Microsoft.TypeSpec.Generator.Tests — 1895/1895 passed
  • Microsoft.TypeSpec.Generator.ClientModel.Tests — 1571/1571 passed
  • eng/scripts/Generate.ps1 — regenerated all libraries, no output drift
  • npm run copcop checks passed.

Follow-up

Azure.Generator in Azure/azure-sdk-for-net derives from ScmTypeFactory, so it picks up System.ClientModel resolution from this change automatically. It will still need the equivalent one-line fallback for its own Azure.Core / Azure.ResourceManager types.

…k types

TypeFactory.CreateFrameworkType falls back to Type.GetType, which only probes
corlib and the assembly declaring the call (Microsoft.TypeSpec.Generator).
System.ClientModel is referenced by Microsoft.TypeSpec.Generator.ClientModel,
so "System.ClientModel.FileBinaryContent" never resolved and last-contract
symbols for it produced a non-framework CSharpType. Back-compat property type
preservation then replaced the generated FileBinaryContent framework type with
the symbol-backed look-alike, which broke IsFileBinaryContentType and dropped
the multipart convenience constructors, the [Experimental] attributes and the
correct MultiPartFormContent.Add overload.

Override CreateFrameworkType in ScmTypeFactory to fall back to
Assembly.GetType scoped to the System.ClientModel assembly.

Fixes microsoft#11676

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 33840ead-d95e-4c5d-91eb-8765e25089bc
@pkg-pr-new

pkg-pr-new Bot commented Aug 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: 5651949

@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.

Pull request overview

This PR fixes nondeterministic framework-type resolution for System.ClientModel types in the http-client-csharp generator by ensuring System.ClientModel.* types can be resolved as framework types during external type binding, preventing back-compat from “downgrading” them into symbol-backed non-framework lookalikes.

Changes:

  • Override framework-type resolution in ScmTypeFactory to fall back to resolving types via the System.ClientModel assembly.
  • Add a regression test ensuring a last-contract FileBinaryContent property remains a framework type and generated output stays stable.
  • Add last-contract and expected-output test assets for the new regression.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmTypeFactory.cs Adds a CreateFrameworkType override to resolve System.ClientModel types via Assembly.GetType.
packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/ScmModelProviderTests.cs Adds a regression test validating back-compat preserves FileBinaryContent as a framework type.
packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/TestData/ScmModelProviderTests/TestMultipartFormDataModel_LastContractFileType_KeepsFileBinaryContentFrameworkType/MultiPartRequest.cs Adds the last-contract source file used to reproduce the type-resolution scenario.
packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/TestData/ScmModelProviderTests/TestMultipartFormDataModel_LastContractFileType_KeepsFileBinaryContentFrameworkType.cs Adds the expected generated output baseline for the regression.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@JoshLove-msft
JoshLove-msft added this pull request to the merge queue Aug 14, 2026
Merged via the queue into microsoft:main with commit ce3f873 Aug 14, 2026
29 checks passed
@JoshLove-msft
JoshLove-msft deleted the fix/11676-filebinarycontent-framework-type branch August 14, 2026 22:32
@JoshLove-msft

Copy link
Copy Markdown
Contributor Author

Verified against the real Azure.Analytics.PlanetaryComputer library

I reproduced issue #11676 end-to-end on the actual SDK and confirmed this PR fixes it.

Repro setup — the bug only appears when a last contract is loaded, which happens when the csproj carries <ApiCompatVersion>. I staged a 1.0.0 baseline package and set <Version>1.0.1</Version> + <ApiCompatVersion>1.0.0</ApiCompatVersion>, mirroring what eng/scripts/Update-PkgVersion.ps1 does during a version bump. That also explains the reported intermittency: no baseline in the NuGet cache ⇒ no last contract ⇒ no bug, which is why only the "increment version" PR failed.

Controlled A/B — I packed two generator package sets from the same commit, one with the fix and one without, and rebuilt Azure.Generator against each, so the only variable is this change.

StacAssetData.cs StacAssetData.Serialization.Multipart.cs PlanetaryComputerModelFactory.cs
without fix −47 lines (lost [Experimental("SCME0004")] + string/Stream/BinaryData ctors) content.Add("file", File)content.Add<FileBinaryContent>(...) −2 lines
with fix clean clean clean

With the fix the only remaining diff is Internal/ModelSerializationExtensions.cs (+55, a WriteBase64StringValue helper) — unrelated version skew between main and the pinned 1.0.0-alpha.20260813.5, and identical in both arms. The regenerated library compiles with 0 errors (it previously failed with 12).

Important

One gotcha worth flagging for the Azure side: AzureTypeFactory already declares protected override Type? CreateFrameworkType(...) ending in base.CreateFrameworkType(...). Since ScmTypeFactory had no such member when Azure.Generator was last compiled, that base. call was emitted as a non-virtual call TypeFactory::CreateFrameworkType and bypasses the new override. Azure.Generator therefore has to be recompiled against a generator build containing this fix — a plain DLL swap is not enough. The normal UnbrandedGeneratorVersion bump handles this, so no action is needed beyond the usual dependency flow.

--generated by Copilot

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.

[http-client-csharp] Multipart model loses [Experimental] attributes and convenience ctors - FileBinaryContent not resolved as a framework type

3 participants