Skip to content

[http-client-csharp] Support accessibility customization through partial constructors - #12528

Open
Jorge Rangel (jorgerangel-msft) with Copilot wants to merge 12 commits into
mainfrom
copilot/support-changing-constructor-accessibility
Open

Jorge Rangel (jorgerangel-msft) with Copilot wants to merge 12 commits into
mainfrom
copilot/support-changing-constructor-accessibility

Conversation

Copilot AI commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Generated methods support accessibility changes through bodyless partial declarations without replacing their implementation. Extend that customization to C# 14 instance constructors.

  • Central constructor merge
    • Adopt custom accessibility and parameter names while retaining generated initializers, bodies, and metadata.
    • Keep optional defaults on the defining declaration only.
  • Isolation and compatibility
    • Clone shared parameters and scope name bindings to avoid altering base or sibling constructors.
    • Preserve explicit partial accessibility during back-compatibility processing.
    • Retain ordinary constructor replacement and suppression behavior.
  • C# 14 parsing
    • Upgrade Roslyn Workspaces from 4.8.0 to 5.9.0, whose default language version recognizes partial constructor declarations.
  • Roslyn simplifier workaround
    • Roslyn 5.x keeps global:: qualifications when it removes a cast that gives a conditional expression its type (dotnet/roslyn#86393). This affects Azure settings constructors, e.g. useKey ? new global::Azure.Core.AzureKeyCredentialPolicy(...) : new global::Azure.Core.Pipeline.BearerTokenAuthenticationPolicy(...); the issue reports 11 changed files across 8 Azure SDK clients.
    • Before the normal reduction, simplify only the qualified names inside conditionals with a typing branch cast (excluding null/default casts). Output matches Roslyn 4.8. disable-roslyn-reduce skips both passes. A TODO tracks removing this once Roslyn ships a fix.
    • Dropping the cast in the generator is not an alternative: cast-free conditionals keep the qualifications on both Roslyn 4.8 and 5.9, and removing the cast can change overload resolution.

Example customization:

public partial class FrontendIPConfiguration
{
    public partial FrontendIPConfiguration(
        string bicepIdentifier, string resourceVersion = null);
}

The matching generated implementation becomes public partial, retaining its generated API-version fallback and body without repeating the optional default.

Performance impact

Measured on a shared Windows development machine with .NET 10, using Sample-TypeSpec (239 generated files). Timings vary between runs, so treat them as indicative.

Workaround cost: Roslyn simplification only, warm process, 18 samples across 3 alternating runs

Configuration Median time Allocated
Roslyn 4.8 (main) 3,743 ms 1,400 MiB
Roslyn 5.9, without workaround 2,917 ms 1,257 MiB
Roslyn 5.9, with workaround 3,088 ms 1,258 MiB
  • The workaround selected 0 of 239 Sample-TypeSpec files, so its cost there is a syntax scan of about 20 ms per library; the two Roslyn 5.9 rows have overlapping interquartile ranges.
  • Each file containing the affected pattern, typically one client file per affected Azure library, pays one extra targeted simplification pass: about 3.5–4.5 ms per file in a 239-file project.

End-to-end generation: real generator on Sample-TypeSpec, fresh process per run, 6 alternating pairs

Generator Median wall time Roslyn post-processing
PR base (Roslyn 4.8) 29.2 s 16.6 s
This PR (Roslyn 5.9 with workaround) 34.4 s 20.7 s
  • Both generators produced byte-identical output for all 240 generated files.
  • The slowdown comes from Roslyn 5.9 startup, not the workaround. In a fresh process, the first simplification of the same 239 files took 19.1–20.3 s on 5.9 versus 13.5–15.0 s on 4.8, while warm runs were faster on 5.9. Neither Roslyn package ships ReadyToRun code, and the 5.9 assemblies are about 10% larger, so each generator process spends more time JIT-compiling. Publishing the generator with ReadyToRun could offset this and is out of scope here.
  • These end-to-end runs predate removing the explicit Preview parse option. A separate comparison of C# 12, C# 14, and Preview parse options found no consistent timing difference and identical output.

Spector scenarios: three Spector projects selected at random (seed 12528), full client generation with ScmCodeModelGenerator, this PR's current head, fresh process per run, 6 alternating pairs each

Scenario Files selected by workaround Median wall-time change Median Roslyn post-processing change
http/payload/pageable 0 of 136 −6.9% −2.9%
http/versioning/removed/v1 0 of 38 +3.2% −1.3%
http/parameters/spread 0 of 42 +8.4% +7.1%
  • Changes are medians of paired PR/base ratios. Across all 18 pairs, this PR was 5.2% slower end to end and 0.9% slower in Roslyn post-processing; it was slower in 11 of 18 pairs, while individual pairs ranged from 41% faster to 75% slower.
  • Generated output was byte-identical in every scenario. On these smaller projects, the difference is within run-to-run noise; the larger Sample-TypeSpec run above shows a clearer slowdown.

Validation

  • Full regeneration of the TypeSpec C# test projects with Roslyn 5.9 and the workaround produced no generated-file diffs. After switching to the default language version, Sample-TypeSpec and Spector special-words were regenerated again with no diffs.
  • Generator, ClientModel, and generated-client unit tests pass, along with Cop and formatting checks.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
1 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI and others added 3 commits November 10, 2026 16:34
Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
Copilot AI changed the title [WIP] Support changing constructor accessibility through partial declarations [http-client-csharp] Support accessibility customization through partial constructors Nov 10, 2026
@pkg-pr-new

pkg-pr-new Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

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

commit: e543ab5

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

Custom constructor parameter names are not preserved exactly, so distinct identifiers such as first_name and firstName can collapse into duplicate names and generate uncompilable C#.

3 open issues
What changed in this PR

Extends the C# client generator’s partial-member customization to C# 14 instance constructors, allowing authors to change accessibility while retaining generated implementations.

Changes:

  • Recognizes and merges partial constructor declarations, preserving generated bodies, initializers, and metadata.
  • Clones parameters and scopes name bindings to isolate customizations; preserves partial accessibility during compatibility processing.
  • Upgrades Roslyn Workspaces to 5.0 and adds parsing, matching, and output tests.

Paths below are relative to packages/http-client-csharp/generator/.

File Description
Packages.Data.props Upgrades Roslyn for C# 14 constructor parsing.
Microsoft.TypeSpec.Generator/​test/​Providers/​TypeProviderTests.cs Tests constructor merging, accessibility, and matching.
Microsoft.TypeSpec.Generator/​test/​Providers/​TestData/​TypeProviderTests/​CustomPartialConstructorsPreserveImplementation/​CustomPartialConstructorType.cs Supplies partial declarations with custom names and defaults.
Microsoft.TypeSpec.Generator/​test/​Providers/​TestData/​TypeProviderTests/​CustomPartialConstructorsPreserveImplementation(True).cs Baselines expression-bodied constructor output.
Microsoft.TypeSpec.Generator/​test/​Providers/​TestData/​TypeProviderTests/​CustomPartialConstructorsPreserveImplementation(False).cs Baselines block-bodied constructor output.
Microsoft.TypeSpec.Generator/​test/​Providers/​TestData/​TypeProviderTests/​CustomPartialConstructorsOnlyMergeMatchingDeclarations/​CustomPartialConstructorType.cs Supplies unmatched, replacement, and suppression cases.
Microsoft.TypeSpec.Generator/​test/​Providers/​NamedTypeSymbolProviders/​TestData/​NamedTypeSymbolProviderTests/​ValidatePartialConstructorDeclarations/​WithPartial.cs Supplies constructor declaration and implementation variants.
Microsoft.TypeSpec.Generator/​test/​Providers/​NamedTypeSymbolProviders/​NamedTypeSymbolProviderTests.cs Tests detection of bodyless partial constructors.
Microsoft.TypeSpec.Generator/​src/​Writers/​CodeWriter.cs Scopes parameter bindings and registers parameter declarations.
Microsoft.TypeSpec.Generator/​src/​Providers/​TypeProvider.cs Centrally merges partial constructors and preserves accessibility.
Microsoft.TypeSpec.Generator/​src/​Providers/​NamedTypeSymbolProvider.cs Recognizes partial constructor declarations.
Microsoft.TypeSpec.Generator/​src/​Providers/​ConstructorProvider.cs Tracks original parameters for scoped bindings.

Review effort: Balanced

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

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 there's test and regen failures. For each commit, we should rerun tests and rerun Generate.ps1

…customization

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

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

Human assessment is needed because constructor matching can conflate distinct parameter passing modes, while accessibility changes can discard existing constructor bodies and initializers.

No open issues 🎉

Review effort: Balanced

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

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

Partial constructor generation can fail compilation due to mismatched explicit accessibility, while compatibility restoration can emit duplicate constructors or lose collection initialization.

2 open issues

Review effort: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

No changes needing a change description found.

Roslyn 5.9 supports partial constructors in its default C# language version, so remove the explicit Preview parsing added for Roslyn 4.14. Restore the nullable assertion required by the simplification workaround.

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

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

Constructor restoration incorrectly replaces generated parameterless constructors customized to internal or private, discarding their initialization logic and leaving optional collection properties null.

No open issues 🎉

2 resolved since last review

Review effort: Balanced

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

Reviewed the generator changes (partial ctor detection, merge in FilterCustomizedConstructors, CodeWriter default omission, back-compat exclusions, Roslyn 5.9 bump plus the simplifier workaround). The core merge is clean, and the test matrix (expression body, shared params, back-compat restore, non-matching types) is good. Two inline findings concern places outside the merge that still see the partial ctor as "custom".

Other notes:

  • PR description is stale. It still says the merge adopts "custom accessibility and parameter names" and "clone[s] shared parameters and scope name bindings". After b987178, only accessibility is merged and the generated parameters are reused unchanged. Please update the description so reviewers and the changelog aren't misled.
  • Roslyn 4.8 → 5.9 is a significant dependency change bundled into a feature PR. Downstream generators (Azure, mgmt) consume Microsoft.TypeSpec.Generator and will pick up Roslyn 5.9 transitively. Your own numbers show about 18% slower end-to-end generation (29.2s → 34.4s), from JIT/startup cost. The extension → @extension escaping (now a contextual keyword) will also churn generated output in every library with a parameter named extension, though it is not source-breaking since named arguments still bind. Consider splitting the bump plus simplifier workaround into its own PR so it can be evaluated and reverted independently.
  • Parameter-name mismatches between the defining declaration and the generated implementation trigger CS8826 in the customer's project. That is a warning, but it becomes an error under TreatWarningsAsErrors. That is probably acceptable, but worth documenting for users of this customization.

--generated by Copilot

MethodSignatureBase.SignatureComparer.Equals(partial.Signature, constructor.Signature));
if (matchingPartial != null)
{
constructors.Add(CreatePartialConstructorFromCustomSignature(matchingPartial.Signature, constructor));

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.

Returning a new ConstructorProvider instance here breaks reference identity with ModelProvider.FullConstructor. ModelFactoryProvider.GetBinaryDataParamAndFullCtorForFactoryMethod (ModelFactoryProvider.cs:847) checks !modelProvider.Constructors.Contains(fullConstructor) to detect a suppressed/replaced full ctor. So when a user only changes the full ctor's accessibility via a partial declaration, the factory now goes down the custom-constructor path. It then searches CanonicalView.Constructors for an Internal ctor with at least as many params. If the partial made it public, that search can pick a different handwritten internal ctor, if one exists, as the factory target.

Suggest either keeping the factory check signature-based, or treating the merged ctor as the full ctor (e.g., Constructors.Any(c => c == fullConstructor || SignatureComparer.Equals(c.Signature, fullConstructor.Signature) && c.BodyStatements == fullConstructor.BodyStatements ...)). A model-factory test with a partial-customized full constructor would also help.

--generated by Copilot

var modifiers = GetAccessModifier(constructorSymbol.DeclaredAccessibility);
if (constructorSymbol.MethodKind == MethodKind.Constructor
&& constructorSymbol.PartialImplementationPart is null
&& IsPartialDeclaration(constructorSymbol))

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.

With this flag, the bodyless defining declaration still stays in CustomCodeView.Constructors. Every consumer outside FilterCustomizedConstructors/ModelProvider back-compat still treats it as a handwritten constructor:

  • ClientSettingsProvider (lines 117, 185) and ConfigurationSchemaGenerator (line 122) read public custom constructors to add settings properties, schema entries, or UnsupportedClientSettingsParameter diagnostics. A client that uses public partial to expose an internal generated ctor (e.g., one taking an AuthenticationPolicy/pipeline) would get spurious settings/schema entries or warnings.
  • CanonicalTypeProvider.BuildConstructors emits both the merged ctor and the partial declaration.

Consider having these consumers skip Partial custom constructors, as ModelProvider now does, or excluding matched partial declarations from the custom view.

--generated by Copilot

This branch has not been deployed

No deployments
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] Support changing constructor accessibility through partial declarations, like regular methods

4 participants