diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index 747e51bcc2f..aeae18ac588 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -1116,6 +1116,11 @@ private static bool TryGetSpecialHeaderParam(InputParameter inputParameter, [Not private static void UpdateParameterNameWithBackCompat(InputParameter inputParameter, string proposedName, TypeProvider backCompatProvider, InputServiceMethod? serviceMethod = null) { + if (inputParameter.IsExactName) + { + return; + } + // Look up the parameter's original (spec) name in the previous contract. // When a service method is supplied, scope the search to methods whose name matches // the current service method (allowing for sync/async pairing) so that a common diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs index 0914920412c..383de9cd653 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs @@ -697,6 +697,32 @@ public async Task ParameterNamePreservedFromLastContractView() "When 'oldParam' is preserved, the renamed 'newParam' must not appear."); } + [Test] + public async Task ExactParameterNameTakesPrecedenceOverLastContractView() + { + var queryParam = InputFactory.QueryParameter( + "oldParam", + InputPrimitiveType.String, + isRequired: true, + isExactName: true); + queryParam.Update(name: "exact_param"); + + var operation = InputFactory.Operation("GetSomething", parameters: [queryParam]); + var serviceMethod = InputFactory.BasicServiceMethod("GetSomething", operation); + var client = InputFactory.Client("TestClient", methods: [serviceMethod]); + + var generator = await MockHelpers.LoadMockGeneratorAsync( + clients: () => [client], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync( + method: nameof(ParameterNamePreservedFromLastContractView))); + + var clientProvider = generator.Object.OutputLibrary.TypeProviders.OfType().First(); + var protocolParams = RestClientProvider.GetMethodParameters(serviceMethod, ScmMethodKind.Protocol, clientProvider); + + Assert.IsNotNull(protocolParams.SingleOrDefault(p => p.Name == "exact_param")); + Assert.IsNull(protocolParams.SingleOrDefault(p => p.Name == "oldParam")); + } + [Test] public async Task ParameterNamePreservedFromInternalLastContractMethod() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs index a8079006dd4..e23b2846199 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs @@ -146,7 +146,8 @@ protected internal sealed override IReadOnlyList BuildMethodsFor { if (currentMethodSignature.Name.Equals(previousMethod.Signature.Name)) { - if (MethodSignatureHelper.HaveSameParametersInSameOrder(currentMethodSignature, previousMethod.Signature)) + if (MethodSignatureHelper.HaveSameParametersInSameOrder(currentMethodSignature, previousMethod.Signature) || + HasMatchingExactParameterNames(currentMethodSignature, previousMethod.Signature)) { foundCompatibleOverload = true; break; @@ -219,6 +220,25 @@ protected internal sealed override IReadOnlyList BuildMethodsFor return [.. factoryMethods]; } + private static bool HasMatchingExactParameterNames(MethodSignature current, MethodSignature previous) + { + if (!MethodSignature.MethodSignatureComparer.Equals(current, previous)) + { + return false; + } + + for (int i = 0; i < current.Parameters.Count; i++) + { + if (current.Parameters[i].Name != previous.Parameters[i].Name && + !current.Parameters[i].IsExactName) + { + return false; + } + } + + return true; + } + internal static IReadOnlyList GetUnavailableSignatureTypes(MethodSignature signature) { var unavailableTypes = new HashSet(StringComparer.Ordinal); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs index b253e910c6f..5c053bedba2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs @@ -56,6 +56,10 @@ public sealed class ParameterProvider : IEquatable /// public InputParameter? InputParameter { get; private set; } + internal bool IsExactName => + InputParameter?.IsExactName == true || + Property?.InputProperty?.IsExactName == true; + /// /// Creates a from an . /// diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs index 979d40194ca..f369f0c4be4 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs @@ -186,6 +186,11 @@ public static void RestorePreviousParameterNames( for (int i = 0; i < currentParameters.Count; i++) { var parameter = currentParameters[i]; + if (parameter.IsExactName) + { + continue; + } + string? preservedName = null; var inputParameter = parameter.InputParameter; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs index 3fd14bf42b4..fc831db1c17 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs @@ -570,6 +570,26 @@ public async Task BackCompatibility_OnlyParamNameChanged() Assert.AreEqual("oldModelProp", docParams[1].Parameter.Name); } + [Test] + public async Task BackCompatibility_ExactPropertyNameTakesPrecedence() + { + var inputModels = GetTestModels(isStringPropExact: true); + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: inputModels, + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync( + method: nameof(BackCompatibility_OnlyParamNameChanged)))).Object; + + var modelFactory = _instance!.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var parameters = modelFactory.Methods + .Single(m => m.Signature.Name == "PublicModel1") + .Signature.Parameters; + Assert.AreEqual("stringProp", parameters[0].Name); + Assert.AreEqual("oldModelProp", parameters[1].Name); + } + // Validates that when ALL parameters in a factory method are renamed in the previous // contract, every preserved name is propagated to the current method. This complements // BackCompatibility_OnlyParamNameChanged which exercises a partial rename. @@ -1250,12 +1270,12 @@ public async Task BackCompatibility_BackCompatMethodCanBeMutatedByVisitor() Assert.IsNotNull(renamed, "The visitor's rename of the back-compat method was not applied."); } - private static InputModelType[] GetTestModels() + private static InputModelType[] GetTestModels(bool isStringPropExact = false) { InputType additionalPropertiesUnknown = InputPrimitiveType.Any; InputModelProperty[] properties = [ - InputFactory.Property("StringProp", InputPrimitiveType.String), + InputFactory.Property("StringProp", InputPrimitiveType.String, isExactName: isStringPropExact), InputFactory.Property("ModelProp", InputFactory.Model("Thing")), InputFactory.Property("ListProp", InputFactory.Array(InputPrimitiveType.String)), InputFactory.Property("DictProp", InputFactory.Dictionary(InputPrimitiveType.String, InputPrimitiveType.String)),