diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index a296420f62f..c829e290267 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -42,6 +42,7 @@ private record ApiVersionFields(FieldProvider Field, PropertyProvider? Correspon private const string ClientSuffix = "Client"; private readonly FormattableString _publicCtorDescription; private readonly InputClient _inputClient; + internal InputClient InputClient => _inputClient; private readonly InputAuth? _inputAuth; private readonly ParameterProvider _endpointParameter; /// 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 b85fc44c6a9..453815faea3 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 @@ -182,7 +182,34 @@ private PropertyProvider FindPropertyInModelHierarchy(TypeProvider model, string protected override string BuildNamespace() => Client.Type.Namespace; protected override string BuildName() - => $"{Client.Type.Name}{Operation.Name.ToIdentifierName()}{(IsAsync ? "Async" : "")}CollectionResult{(ItemModelType == null ? "" : "OfT")}"; + { + var operationName = Operation.Name.ToIdentifierName(); + // Check if there is another paging operation in the same client whose name would produce a collision. + // If so, use the OriginalName to differentiate. + if (HasPagingOperationNameCollision(operationName)) + { + operationName = (Operation.OriginalName ?? Operation.Name).ToIdentifierName(); + } + return $"{Client.Type.Name}{operationName}{(IsAsync ? "Async" : "")}CollectionResult{(ItemModelType == null ? "" : "OfT")}"; + } + + private bool HasPagingOperationNameCollision(string operationName) + { + var pagingMethods = Client.InputClient.Methods.OfType(); + int count = 0; + foreach (var method in pagingMethods) + { + if (method.Operation.Name.ToIdentifierName() == operationName) + { + count++; + if (count > 1) + { + return true; + } + } + } + return false; + } protected override TypeSignatureModifiers BuildDeclarationModifiers() => TypeSignatureModifiers.Internal | TypeSignatureModifiers.Partial | TypeSignatureModifiers.Class; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/CollectionResultDefinitionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/CollectionResultDefinitionTests.cs index 6947ee8104e..cd8683227d4 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/CollectionResultDefinitionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/CollectionResultDefinitionTests.cs @@ -205,6 +205,81 @@ public void TestEmptyStringHandlingForUriNextLink() "Generated code should check for null URI"); } + [Test] + public void TestCollectionResultNamesDoNotCollideWhenOperationsAreRenamed() + { + // Two paging operations "list" and "listAll" both get renamed to "GetAll" by CleanOperationNames. + // The CollectionResult names should use OriginalName to avoid collision. + var thingModel = InputFactory.Model("thing", properties: + [ + InputFactory.Property("name", InputPrimitiveType.String, isRequired: true), + ]); + var thingsProperty = InputFactory.Property("things", InputFactory.Array(thingModel)); + var nextProperty = InputFactory.Property("next", InputPrimitiveType.Url); + var pageModel = InputFactory.Model("page", properties: [thingsProperty, nextProperty]); + var response = InputFactory.OperationResponse([200], pageModel); + + var pagingMetadata = InputFactory.NextLinkPagingMetadata(["things"], ["next"], InputResponseLocation.Body); + + // "list" will be renamed to "GetAll", "listAll" will also be renamed to "GetAll" + var listOperation = InputFactory.Operation("list", responses: [response]); + var listAllOperation = InputFactory.Operation("listAll", responses: [response]); + + var listServiceMethod = InputFactory.PagingServiceMethod("list", listOperation, pagingMetadata: pagingMetadata); + var listAllServiceMethod = InputFactory.PagingServiceMethod("listAll", listAllOperation, pagingMetadata: pagingMetadata); + + var client = InputFactory.Client("FooClient", methods: [listServiceMethod, listAllServiceMethod]); + + MockHelpers.LoadMockGenerator(inputModels: () => [thingModel], clients: () => [client]); + + var collectionResults = ScmCodeModelGenerator.Instance.OutputLibrary.TypeProviders + .Where(t => t is CollectionResultDefinition) + .ToList(); + + // Should have 8 CollectionResult types (2 ops × 2 sync/async × 2 typed/untyped) and they should all have unique names + Assert.AreEqual(8, collectionResults.Count, + $"Expected 8 CollectionResult types but found {collectionResults.Count}"); + var collectionResultNames = collectionResults.Select(t => t.Name).ToList(); + Assert.AreEqual(collectionResultNames.Distinct().Count(), collectionResultNames.Count, + $"CollectionResult names should be unique but found duplicates: {string.Join(", ", collectionResultNames)}"); + + // Both should use the original names for disambiguation + Assert.IsTrue(collectionResultNames.Any(n => n == "FooClientListCollectionResult"), + $"Expected 'FooClientListCollectionResult' in [{string.Join(", ", collectionResultNames)}]"); + Assert.IsTrue(collectionResultNames.Any(n => n == "FooClientListAllCollectionResult"), + $"Expected 'FooClientListAllCollectionResult' in [{string.Join(", ", collectionResultNames)}]"); + } + + [Test] + public void TestCollectionResultNameUsesCurrentNameWhenNoCollision() + { + // A single paging operation should use the current (cleaned) name, not the original name. + var thingModel = InputFactory.Model("thing", properties: + [ + InputFactory.Property("name", InputPrimitiveType.String, isRequired: true), + ]); + var thingsProperty = InputFactory.Property("things", InputFactory.Array(thingModel)); + var nextProperty = InputFactory.Property("next", InputPrimitiveType.Url); + var pageModel = InputFactory.Model("page", properties: [thingsProperty, nextProperty]); + var response = InputFactory.OperationResponse([200], pageModel); + + var pagingMetadata = InputFactory.NextLinkPagingMetadata(["things"], ["next"], InputResponseLocation.Body); + + // "listAll" gets renamed to "GetAll" by CleanOperationNames, no collision + var listAllOperation = InputFactory.Operation("listAll", responses: [response]); + var listAllServiceMethod = InputFactory.PagingServiceMethod("listAll", listAllOperation, pagingMetadata: pagingMetadata); + + var client = InputFactory.Client("FooClient", methods: [listAllServiceMethod]); + + MockHelpers.LoadMockGenerator(inputModels: () => [thingModel], clients: () => [client]); + + // When there's no collision, the cleaned name "GetAll" should be used + var collectionResultDefinition = ScmCodeModelGenerator.Instance.OutputLibrary.TypeProviders.FirstOrDefault( + t => t is CollectionResultDefinition && t.Name == "FooClientGetAllCollectionResult") as CollectionResultDefinition; + Assert.IsNotNull(collectionResultDefinition, + "CollectionResult should use cleaned name 'GetAll' when there's no collision"); + } + internal static void CreatePagingOperation(InputResponseLocation responseLocation, bool isNested = false) { var inputModel = InputFactory.Model("cat", properties: diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputOperation.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputOperation.cs index 54f37df38db..6db80119d68 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputOperation.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputOperation.cs @@ -73,6 +73,11 @@ public InputOperation() : this( { } public string Name { get; internal set; } + + /// + /// Gets the original name of the operation as defined in the TypeSpec before any mutations. + /// + public string? OriginalName { get; internal set; } public string? ResourceName { get; internal set; } public string? Summary { get; internal set; } public string? Doc { get; internal set; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/InputOperationConverter.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/InputOperationConverter.cs index 28b6a06c988..32e27880757 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/InputOperationConverter.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/InputOperationConverter.cs @@ -87,6 +87,7 @@ public override void Write(Utf8JsonWriter writer, InputOperation value, JsonSeri } operation.Name = name ?? throw new JsonException("InputOperation must have name"); + operation.OriginalName = name; operation.ResourceName = resourceName; operation.Summary = summary; operation.Doc = doc; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/common/InputFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/common/InputFactory.cs index 53c8b2228c8..85632b4ee25 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/common/InputFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/common/InputFactory.cs @@ -639,7 +639,7 @@ public static InputOperation Operation( bool generateConvenienceMethod = true, string? ns = null) { - return new InputOperation( + var operation = new InputOperation( name, null, "", @@ -658,6 +658,8 @@ public static InputOperation Operation( generateConvenienceMethod, name, ns); + operation.OriginalName = name; + return operation; } public static InputPagingServiceMetadata NextLinkPagingMetadata(