From 40f93378d0c04f53147bf232ddd149adcd5ccd51 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 20 Jan 2026 14:39:02 +0000
Subject: [PATCH 1/2] Initial plan
From e0e5eea629693d1ca77f65e7b38d0bee65a634c2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 20 Jan 2026 14:51:51 +0000
Subject: [PATCH 2/2] Fix nested nextLink property lookup for inherited models
Co-authored-by: ArthurMa1978 <20514459+ArthurMa1978@users.noreply.github.com>
---
.../Providers/CollectionResultDefinition.cs | 25 +++++-
.../NextLinkTests.cs | 80 +++++++++++++++++++
.../NextLinkTests/InheritedNextLinkInBody.cs | 55 +++++++++++++
.../InheritedNextLinkInBodyAsync.cs | 55 +++++++++++++
.../InheritedNextLinkInBodyOfT.cs | 60 ++++++++++++++
.../InheritedNextLinkInBodyOfTAsync.cs | 65 +++++++++++++++
.../src/Snippets/ModelProviderSnippets.cs | 25 +++++-
7 files changed, 363 insertions(+), 2 deletions(-)
create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/NextLinkTests/InheritedNextLinkInBody.cs
create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/NextLinkTests/InheritedNextLinkInBodyAsync.cs
create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/NextLinkTests/InheritedNextLinkInBodyOfT.cs
create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/NextLinkTests/InheritedNextLinkInBodyOfTAsync.cs
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs
index 9920e8b219a..9d631107d0c 100644
--- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs
@@ -143,7 +143,7 @@ private CSharpType GetNextPagePropertyType()
PropertyProvider? property = null;
for (int i = 0; i < NextPagePropertySegments.Count; i++)
{
- property = model.Properties.First(p => p.WireInfo?.SerializedName == NextPagePropertySegments[i]);
+ property = FindPropertyInModelHierarchy(model, NextPagePropertySegments[i]);
if (i < NextPagePropertySegments.Count - 1)
{
@@ -154,6 +154,29 @@ private CSharpType GetNextPagePropertyType()
return property!.Type;
}
+ ///
+ /// Searches for a property with the specified serialized name in the model and its base models.
+ ///
+ private PropertyProvider FindPropertyInModelHierarchy(TypeProvider model, string serializedName)
+ {
+ // First, try to find the property in the current model
+ var property = model.Properties.FirstOrDefault(p => p.WireInfo?.SerializedName == serializedName);
+ if (property != null)
+ {
+ return property;
+ }
+
+ // If not found, search in the base model hierarchy
+ if (model is ModelProvider modelProvider && modelProvider.BaseModelProvider != null)
+ {
+ return FindPropertyInModelHierarchy(modelProvider.BaseModelProvider, serializedName);
+ }
+
+ // If not found anywhere, throw an exception with a helpful message
+ throw new InvalidOperationException(
+ $"Property with serialized name '{serializedName}' not found in model '{model.Name}' or its base models.");
+ }
+
protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", "CollectionResults", $"{Name}.cs");
protected override string BuildNamespace() => Client.Type.Namespace;
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/NextLinkTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/NextLinkTests.cs
index e16053f968b..1f3be8fcf94 100644
--- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/NextLinkTests.cs
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/NextLinkTests.cs
@@ -238,6 +238,62 @@ public void UsesValidFieldIdentifierNames()
Assert.IsTrue(fields.Any(f => f.Name == "_foo"));
}
+ [Test]
+ public void InheritedNextLinkInBody()
+ {
+ CreatePagingOperationWithInheritedNextLink(InputResponseLocation.Body);
+
+ var collectionResultDefinition = ScmCodeModelGenerator.Instance.OutputLibrary.TypeProviders.FirstOrDefault(
+ t => t is CollectionResultDefinition && t.Name == "CatClientGetCatsCollectionResult");
+ Assert.IsNotNull(collectionResultDefinition);
+
+ var writer = new TypeProviderWriter(collectionResultDefinition!);
+ var file = writer.Write();
+ Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
+ }
+
+ [Test]
+ public void InheritedNextLinkInBodyAsync()
+ {
+ CreatePagingOperationWithInheritedNextLink(InputResponseLocation.Body);
+
+ var collectionResultDefinition = ScmCodeModelGenerator.Instance.OutputLibrary.TypeProviders.FirstOrDefault(
+ t => t is CollectionResultDefinition && t.Name == "CatClientGetCatsAsyncCollectionResult");
+ Assert.IsNotNull(collectionResultDefinition);
+
+ var writer = new TypeProviderWriter(collectionResultDefinition!);
+ var file = writer.Write();
+ Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
+ }
+
+ [Test]
+ public void InheritedNextLinkInBodyOfT()
+ {
+ CreatePagingOperationWithInheritedNextLink(InputResponseLocation.Body);
+
+ var collectionResultDefinition = ScmCodeModelGenerator.Instance.OutputLibrary.TypeProviders.FirstOrDefault(
+ t => t is CollectionResultDefinition && t.Name == "CatClientGetCatsCollectionResultOfT");
+ Assert.IsNotNull(collectionResultDefinition);
+
+ var writer = new TypeProviderWriter(collectionResultDefinition!);
+ var file = writer.Write();
+ Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
+ }
+
+ [Test]
+ public void InheritedNextLinkInBodyOfTAsync()
+ {
+ CreatePagingOperationWithInheritedNextLink(InputResponseLocation.Body);
+
+ var collectionResultDefinition = ScmCodeModelGenerator.Instance.OutputLibrary.TypeProviders.FirstOrDefault(
+ t => t is CollectionResultDefinition && t.Name == "CatClientGetCatsAsyncCollectionResultOfT");
+ Assert.IsNotNull(collectionResultDefinition);
+
+ var writer = new TypeProviderWriter(collectionResultDefinition!);
+ var file = writer.Write();
+ Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
+ }
+
private static void CreatePagingOperation(InputResponseLocation responseLocation, bool isNested = false)
{
var inputModel = InputFactory.Model("cat", properties:
@@ -267,5 +323,29 @@ private static void CreatePagingOperation(InputResponseLocation responseLocation
MockHelpers.LoadMockGenerator(inputModels: () => [inputModel], clients: () => [client]);
}
+
+ private static void CreatePagingOperationWithInheritedNextLink(InputResponseLocation responseLocation)
+ {
+ var inputModel = InputFactory.Model("cat", properties:
+ [
+ InputFactory.Property("color", InputPrimitiveType.String, isRequired: true),
+ ]);
+
+ // Create the base model with nextLink property
+ var nextCatProperty = InputFactory.Property("nextCat", InputPrimitiveType.Url);
+ var baseModel = InputFactory.Model("basePage", properties: [nextCatProperty]);
+
+ // Create the derived model that inherits from base and adds cats property
+ var catsProperty = InputFactory.Property("cats", InputFactory.Array(inputModel));
+ var derivedModel = InputFactory.Model("page", properties: [catsProperty], baseModel: baseModel);
+
+ var pagingMetadata = InputFactory.NextLinkPagingMetadata(["cats"], ["nextCat"], responseLocation);
+ var response = InputFactory.OperationResponse([200], derivedModel);
+ var operation = InputFactory.Operation("getCats", responses: [response]);
+ var inputServiceMethod = InputFactory.PagingServiceMethod("getCats", operation, pagingMetadata: pagingMetadata);
+ var client = InputFactory.Client("catClient", methods: [inputServiceMethod]);
+
+ MockHelpers.LoadMockGenerator(inputModels: () => [inputModel, baseModel, derivedModel], clients: () => [client]);
+ }
}
}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/NextLinkTests/InheritedNextLinkInBody.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/NextLinkTests/InheritedNextLinkInBody.cs
new file mode 100644
index 00000000000..06a5525b8d9
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/NextLinkTests/InheritedNextLinkInBody.cs
@@ -0,0 +1,55 @@
+//
+
+#nullable disable
+
+using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using System.Collections.Generic;
+using Sample.Models;
+
+namespace Sample
+{
+ internal partial class CatClientGetCatsCollectionResult : global::System.ClientModel.Primitives.CollectionResult
+ {
+ private readonly global::Sample.CatClient _client;
+ private readonly global::System.ClientModel.Primitives.RequestOptions _options;
+
+ public CatClientGetCatsCollectionResult(global::Sample.CatClient client, global::System.ClientModel.Primitives.RequestOptions options)
+ {
+ _client = client;
+ _options = options;
+ }
+
+ public override global::System.Collections.Generic.IEnumerable GetRawPages()
+ {
+ global::System.ClientModel.Primitives.PipelineMessage message = _client.CreateGetCatsRequest(_options);
+ global::System.Uri nextPageUri = null;
+ while (true)
+ {
+ global::System.ClientModel.ClientResult result = global::System.ClientModel.ClientResult.FromResponse(_client.Pipeline.ProcessMessage(message, _options));
+ yield return result;
+
+ nextPageUri = ((global::Sample.Models.Page)result).NextCat;
+ if ((nextPageUri == null))
+ {
+ yield break;
+ }
+ message = _client.CreateNextGetCatsRequest(nextPageUri, _options);
+ }
+ }
+
+ public override global::System.ClientModel.ContinuationToken GetContinuationToken(global::System.ClientModel.ClientResult page)
+ {
+ global::System.Uri nextPage = ((global::Sample.Models.Page)page).NextCat;
+ if ((nextPage != null))
+ {
+ return global::System.ClientModel.ContinuationToken.FromBytes(global::System.BinaryData.FromString(nextPage.AbsoluteUri));
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/NextLinkTests/InheritedNextLinkInBodyAsync.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/NextLinkTests/InheritedNextLinkInBodyAsync.cs
new file mode 100644
index 00000000000..89813e7a174
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/NextLinkTests/InheritedNextLinkInBodyAsync.cs
@@ -0,0 +1,55 @@
+//
+
+#nullable disable
+
+using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using System.Collections.Generic;
+using Sample.Models;
+
+namespace Sample
+{
+ internal partial class CatClientGetCatsAsyncCollectionResult : global::System.ClientModel.Primitives.AsyncCollectionResult
+ {
+ private readonly global::Sample.CatClient _client;
+ private readonly global::System.ClientModel.Primitives.RequestOptions _options;
+
+ public CatClientGetCatsAsyncCollectionResult(global::Sample.CatClient client, global::System.ClientModel.Primitives.RequestOptions options)
+ {
+ _client = client;
+ _options = options;
+ }
+
+ public override async global::System.Collections.Generic.IAsyncEnumerable GetRawPagesAsync()
+ {
+ global::System.ClientModel.Primitives.PipelineMessage message = _client.CreateGetCatsRequest(_options);
+ global::System.Uri nextPageUri = null;
+ while (true)
+ {
+ global::System.ClientModel.ClientResult result = global::System.ClientModel.ClientResult.FromResponse(await _client.Pipeline.ProcessMessageAsync(message, _options).ConfigureAwait(false));
+ yield return result;
+
+ nextPageUri = ((global::Sample.Models.Page)result).NextCat;
+ if ((nextPageUri == null))
+ {
+ yield break;
+ }
+ message = _client.CreateNextGetCatsRequest(nextPageUri, _options);
+ }
+ }
+
+ public override global::System.ClientModel.ContinuationToken GetContinuationToken(global::System.ClientModel.ClientResult page)
+ {
+ global::System.Uri nextPage = ((global::Sample.Models.Page)page).NextCat;
+ if ((nextPage != null))
+ {
+ return global::System.ClientModel.ContinuationToken.FromBytes(global::System.BinaryData.FromString(nextPage.AbsoluteUri));
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/NextLinkTests/InheritedNextLinkInBodyOfT.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/NextLinkTests/InheritedNextLinkInBodyOfT.cs
new file mode 100644
index 00000000000..1ccd43e7e51
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/NextLinkTests/InheritedNextLinkInBodyOfT.cs
@@ -0,0 +1,60 @@
+//
+
+#nullable disable
+
+using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using System.Collections.Generic;
+using Sample.Models;
+
+namespace Sample
+{
+ internal partial class CatClientGetCatsCollectionResultOfT : global::System.ClientModel.CollectionResult
+ {
+ private readonly global::Sample.CatClient _client;
+ private readonly global::System.ClientModel.Primitives.RequestOptions _options;
+
+ public CatClientGetCatsCollectionResultOfT(global::Sample.CatClient client, global::System.ClientModel.Primitives.RequestOptions options)
+ {
+ _client = client;
+ _options = options;
+ }
+
+ public override global::System.Collections.Generic.IEnumerable GetRawPages()
+ {
+ global::System.ClientModel.Primitives.PipelineMessage message = _client.CreateGetCatsRequest(_options);
+ global::System.Uri nextPageUri = null;
+ while (true)
+ {
+ global::System.ClientModel.ClientResult result = global::System.ClientModel.ClientResult.FromResponse(_client.Pipeline.ProcessMessage(message, _options));
+ yield return result;
+
+ nextPageUri = ((global::Sample.Models.Page)result).NextCat;
+ if ((nextPageUri == null))
+ {
+ yield break;
+ }
+ message = _client.CreateNextGetCatsRequest(nextPageUri, _options);
+ }
+ }
+
+ public override global::System.ClientModel.ContinuationToken GetContinuationToken(global::System.ClientModel.ClientResult page)
+ {
+ global::System.Uri nextPage = ((global::Sample.Models.Page)page).NextCat;
+ if ((nextPage != null))
+ {
+ return global::System.ClientModel.ContinuationToken.FromBytes(global::System.BinaryData.FromString(nextPage.AbsoluteUri));
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ protected override global::System.Collections.Generic.IEnumerable GetValuesFromPage(global::System.ClientModel.ClientResult page)
+ {
+ return ((global::Sample.Models.Page)page).Cats;
+ }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/NextLinkTests/InheritedNextLinkInBodyOfTAsync.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/NextLinkTests/InheritedNextLinkInBodyOfTAsync.cs
new file mode 100644
index 00000000000..fedf5d6934e
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/NextLinkTests/InheritedNextLinkInBodyOfTAsync.cs
@@ -0,0 +1,65 @@
+//
+
+#nullable disable
+
+using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Sample.Models;
+
+namespace Sample
+{
+ internal partial class CatClientGetCatsAsyncCollectionResultOfT : global::System.ClientModel.AsyncCollectionResult
+ {
+ private readonly global::Sample.CatClient _client;
+ private readonly global::System.ClientModel.Primitives.RequestOptions _options;
+
+ public CatClientGetCatsAsyncCollectionResultOfT(global::Sample.CatClient client, global::System.ClientModel.Primitives.RequestOptions options)
+ {
+ _client = client;
+ _options = options;
+ }
+
+ public override async global::System.Collections.Generic.IAsyncEnumerable GetRawPagesAsync()
+ {
+ global::System.ClientModel.Primitives.PipelineMessage message = _client.CreateGetCatsRequest(_options);
+ global::System.Uri nextPageUri = null;
+ while (true)
+ {
+ global::System.ClientModel.ClientResult result = global::System.ClientModel.ClientResult.FromResponse(await _client.Pipeline.ProcessMessageAsync(message, _options).ConfigureAwait(false));
+ yield return result;
+
+ nextPageUri = ((global::Sample.Models.Page)result).NextCat;
+ if ((nextPageUri == null))
+ {
+ yield break;
+ }
+ message = _client.CreateNextGetCatsRequest(nextPageUri, _options);
+ }
+ }
+
+ public override global::System.ClientModel.ContinuationToken GetContinuationToken(global::System.ClientModel.ClientResult page)
+ {
+ global::System.Uri nextPage = ((global::Sample.Models.Page)page).NextCat;
+ if ((nextPage != null))
+ {
+ return global::System.ClientModel.ContinuationToken.FromBytes(global::System.BinaryData.FromString(nextPage.AbsoluteUri));
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ protected override async global::System.Collections.Generic.IAsyncEnumerable GetValuesFromPageAsync(global::System.ClientModel.ClientResult page)
+ {
+ foreach (global::Sample.Models.Cat item in ((global::Sample.Models.Page)page).Cats)
+ {
+ yield return item;
+ await global::System.Threading.Tasks.Task.Yield();
+ }
+ }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Snippets/ModelProviderSnippets.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Snippets/ModelProviderSnippets.cs
index 3c43df0825e..8b7b9466717 100644
--- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Snippets/ModelProviderSnippets.cs
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Snippets/ModelProviderSnippets.cs
@@ -28,7 +28,7 @@ private static ValueExpression BuildPropertyAccessExpression(this ModelProvider
for (int i = 0; i < propertySegments.Count; i++)
{
- var property = currentModel.Properties.First(p => p.WireInfo?.SerializedName == propertySegments[i]);
+ var property = FindPropertyInModelHierarchy(currentModel, propertySegments[i]);
propertyAccessExpression = propertyAccessExpression.Property(property.Name);
@@ -45,6 +45,29 @@ private static ValueExpression BuildPropertyAccessExpression(this ModelProvider
return propertyAccessExpression;
}
+ ///
+ /// Searches for a property with the specified serialized name in the model and its base models.
+ ///
+ private static PropertyProvider FindPropertyInModelHierarchy(TypeProvider model, string serializedName)
+ {
+ // First, try to find the property in the current model
+ var property = model.Properties.FirstOrDefault(p => p.WireInfo?.SerializedName == serializedName);
+ if (property != null)
+ {
+ return property;
+ }
+
+ // If not found, search in the base model hierarchy
+ if (model is ModelProvider modelProvider && modelProvider.BaseModelProvider != null)
+ {
+ return FindPropertyInModelHierarchy(modelProvider.BaseModelProvider, serializedName);
+ }
+
+ // If not found anywhere, throw an exception with a helpful message
+ throw new System.InvalidOperationException(
+ $"Property with serialized name '{serializedName}' not found in model '{model.Name}' or its base models.");
+ }
+
private static bool NeedsNullableConditional(PropertyProvider property)
{
return !property.Type.IsValueType || property.InputProperty?.Type is InputNullableType;