Skip to content

feat: resolve $dynamicRef generic bindings with per-context classes#7978

Open
aqeelat wants to merge 2 commits into
microsoft:mainfrom
aqeelat:feat/dynamicref-generic-binding
Open

feat: resolve $dynamicRef generic bindings with per-context classes#7978
aqeelat wants to merge 2 commits into
microsoft:mainfrom
aqeelat:feat/dynamicref-generic-binding

Conversation

@aqeelat

@aqeelat aqeelat commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 2 of #7815. Builds on Phase 1 (#7817) which resolved recursive $dynamicRef via a thread-local dynamic-scope stack.

This PR adds support for the template binding pattern: when the same template schema is referenced from multiple contexts with different $defs + $dynamicAnchor bindings, each binding context now produces a distinct concrete class instead of silently sharing one class with the first binding's types. This PR emits concrete specializations (e.g., PaginatedTemplateUser, PaginatedTemplateGroup), not reusable generic type declarations.

What changed

KiotaBuilder.cs:

  • DynamicScopeFrame(Schema, BindingSuffix) record replaces bare IOpenApiSchema on the dynamic-scope stack, carrying the computed suffix alongside each frame.
  • TryGetDynamicBindingSuffix — when a schema carries $defs entries with $dynamicAnchor, produces a suffix from the bound type names (or route segment + operation context for inline bindings). Definitions are sorted by key for deterministic output. Context suffix is computed once, not duplicated per anchor.
  • ContainsDynamicReference + GetActiveDynamicBindingSuffix — when a component has no local $defs but contains a reachable $dynamicRef, inherits the binding suffix from the active dynamic scope.
  • Dynamic-ref resolution refactored to a local function. Tries each scope frame as-is first (carries binding $defs), then the unwrapped target. Recursive check runs before GetExistingDeclaration so a bare class can't bypass the suffix.
  • CreateCollectionModelDeclaration conditionally pushes onto the dynamic scope when the array schema carries $defs with $dynamicAnchor.
  • ExtractAnchorName changed from private to internal for unit testing.

Test coverage

Fixture Pattern Languages
generic-binding.yaml $ref bindings in $defs 5
inline-binding.yaml Inline schema bindings (no $ref) 5
recursive-generic-binding.yaml Combined recursive + generic 5
request-body-generic-binding.yaml Bindings in request body 5
multi-anchor-generic-binding.yaml Two $dynamicAnchor slots per template 5
array-root-dynamicref.yaml $dynamicRef in array items at response root 5
unresolved-dynamicref.yaml Unresolved $dynamicRefUntypedNode C#
multi-inline-binding.yaml Multiple inline anchors, context dedup C#
mixed-anchor-binding.yaml Mixed $ref + inline anchors, ordering C#
inherited-generic-binding.yaml Inherited template (allOf) with binding 5
multi-error-inline-binding.yaml Multiple error response bindings C#
request-response-inline-binding.yaml Same route, request vs response bindings C#
inherited-component-binding.yaml Nested component inherits binding identity C#
no-operation-id-inline-binding.yaml Disambiguation without operationId C#
namespaced-binding.yaml Qualified ref IDs (v1.User vs v2.User) C#

Five-language fixtures assert language-specific typed deserialization. C#-only fixtures assert builder-level model identity and binding-specific type resolution.

ExtractAnchorName unit tests added in KiotaBuilderDynamicRefTests.cs.

Behavioral change

Referenced schemas whose site declares $defs entries containing $dynamicAnchor + $ref now get a suffixed class name. This is the intended fix but will rename classes for any existing spec that happens to match this pattern.

Remaining limitations (Phase 3+)

  • An unresolved multi-candidate $dynamicRef with no active binding context still degrades to UntypedNode with a warning.
  • Generic type declarations (Template<T>) are not emitted; each binding produces a concrete class.

Copilot AI review requested due to automatic review settings July 22, 2026 11:47

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@aqeelat
aqeelat force-pushed the feat/dynamicref-generic-binding branch from 5a95ec9 to 2dff270 Compare July 22, 2026 15:18
Copilot AI review requested due to automatic review settings July 22, 2026 15:18

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings July 22, 2026 19:59
@aqeelat
aqeelat force-pushed the feat/dynamicref-generic-binding branch from 2dff270 to 7465ee3 Compare July 22, 2026 19:59

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@aqeelat
aqeelat marked this pull request as ready for review July 22, 2026 20:01
@aqeelat
aqeelat requested a review from a team as a code owner July 22, 2026 20:02
@gavinbarron
gavinbarron requested a review from Copilot July 23, 2026 01:19

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

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (3)

tests/Kiota.Builder.IntegrationTests/GenerateSample.cs:548

  • These assertions only guard against an unsuffixed TreeTemplate in languages that emit the class keyword. For Go, an unsuffixed template would be emitted as type TreeTemplate struct, which isn’t currently checked.
        Assert.DoesNotContain("class TreeTemplate\n", allModelText, StringComparison.Ordinal);
        Assert.DoesNotContain("class TreeTemplate ", allModelText, StringComparison.Ordinal);
        Assert.DoesNotContain("class TreeTemplate:", allModelText, StringComparison.Ordinal);

tests/Kiota.Builder.IntegrationTests/GenerateSample.cs:575

  • These assertions only check for class SearchTemplate..., which won’t catch an unsuffixed Go declaration (type SearchTemplate struct). Adding the Go-specific pattern makes the per-language guarantee more robust.
        Assert.DoesNotContain("class SearchTemplate\n", allModelText, StringComparison.Ordinal);
        Assert.DoesNotContain("class SearchTemplate ", allModelText, StringComparison.Ordinal);
        Assert.DoesNotContain("class SearchTemplate:", allModelText, StringComparison.Ordinal);

tests/Kiota.Builder.IntegrationTests/GenerateSample.cs:603

  • Same issue as other template checks: class EnvelopeTemplate... won’t detect an unsuffixed Go model (type EnvelopeTemplate struct).
        Assert.DoesNotContain("class EnvelopeTemplate\n", allModelText, StringComparison.Ordinal);
        Assert.DoesNotContain("class EnvelopeTemplate ", allModelText, StringComparison.Ordinal);
        Assert.DoesNotContain("class EnvelopeTemplate:", allModelText, StringComparison.Ordinal);

Comment thread tests/Kiota.Builder.Tests/KiotaBuilderDynamicRefTests.cs Outdated
Comment thread tests/Kiota.Builder.IntegrationTests/GenerateSample.cs
Comment thread src/Kiota.Builder/KiotaBuilder.cs Outdated
Copilot AI review requested due to automatic review settings July 23, 2026 09:27

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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

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

Comments suppressed due to low confidence (1)

tests/Kiota.Builder.Tests/KiotaBuilderDynamicRefTests.cs:16

  • KiotaBuilder is referenced without importing the Kiota.Builder namespace, so this new test file won’t compile (unless a global using exists, which it doesn’t in this test project). Add using Kiota.Builder; or fully-qualify the type.
    public void ExtractAnchorNameReturnsExpectedValue(string dynamicRef, string expected)
    {
        Assert.Equal(expected, KiotaBuilder.ExtractAnchorName(dynamicRef));
    }

Comment thread src/Kiota.Builder/KiotaBuilder.cs Outdated
Copilot AI review requested due to automatic review settings July 23, 2026 22:06
@aqeelat
aqeelat force-pushed the feat/dynamicref-generic-binding branch from a15bbd1 to bbc5397 Compare July 23, 2026 22:06

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…classes

test(builder): expand dynamic-ref coverage and fix review items
@aqeelat
aqeelat force-pushed the feat/dynamicref-generic-binding branch from bbc5397 to 354fc0d Compare July 23, 2026 22:12
@baywet
baywet requested a review from Copilot July 24, 2026 12:57

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

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

Comment thread src/Kiota.Builder/KiotaBuilder.cs Outdated
Copilot AI review requested due to automatic review settings July 24, 2026 17:50
@aqeelat
aqeelat force-pushed the feat/dynamicref-generic-binding branch from 354fc0d to 6772aaf Compare July 24, 2026 17:50

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Comment on lines +711 to +715
var allText = ReadGeneratedModelText(Path.Combine(Directory.GetCurrentDirectory(), "Generated", "ArrayRootDynamicRef", language.ToString()));
Assert.DoesNotContain("UntypedNode", allText, StringComparison.Ordinal);
Assert.Contains("User", allText, StringComparison.Ordinal);
Assert.Contains("Group", allText, StringComparison.Ordinal);
}
Comment on lines +754 to +755
// Mixed anchors: one $ref (zRef → User) + one inline (aInline).
// Ref suffixes come first (sorted), context appended once after.
Comment thread src/Kiota.Builder/KiotaBuilder.cs Outdated
// If typeNameForInlineSchema is not null and the schema is referenced, we have most likely unwrapped a referenced schema(most likely from an AllOf/OneOf/AnyOf).
// Therefore the current type/schema is not really inlined, so invalidate the typeNameForInlineSchema and just work with the information from the schema reference.
if (schema.IsReferencedSchema() && !string.IsNullOrEmpty(typeNameForInlineSchema))
// Unresolved $dynamicRef is the exception: its active binding can resolve to an inline schema, so keep the caller-provided inline name.

@baywet baywet left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the contribution!

Thanks for iterating with copilot here.
Beyond the comment I left, and the last few from copilot, I think we're getting really close!

Comment thread src/Kiota.Builder/KiotaBuilder.cs Outdated
if (!string.IsNullOrEmpty(operation?.OperationId))
suffix += operation.OperationId.CleanupSymbolName().ToFirstCharacterUpperCase();
if (isRequestBody)
suffix += "RequestBody";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can we move those to consts if we haven't already defined them? and replace other usage points?

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.

Done. RequestBodySuffix and ResponseSuffix are constants alongside the existing field-level constants, and the corresponding inline literals now use them.

@baywet

baywet commented Jul 24, 2026

Copy link
Copy Markdown
Member

(we'll also need a changelog entry)

@aqeelat

aqeelat commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

@baywet can you ask copilot to do a thorough review? It might speed up the process to get all feedback at once.

@baywet

baywet commented Jul 24, 2026

Copy link
Copy Markdown
Member

@aqeelat not through the UI at least. Maybe tweaking things like the agents.md or the copilot instructions though. But I suspect they designed the feature this way to avoid overwhelming contributors. In my experience most people don't like to get more than 10 or so comments at once. And there's an aspect of importance first (no point telling you about typos in comments if you're going to need to revamp the method anyway).

I'll set up a workflow about the changelog entry requirement though, I've typed that so many times over the years I can't imagine how it's not automated yet.

Copilot AI review requested due to automatic review settings July 25, 2026 14:51
@aqeelat
aqeelat force-pushed the feat/dynamicref-generic-binding branch from 6772aaf to 1668b2f Compare July 25, 2026 14:51

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants