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;