diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientOptionsProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientOptionsProvider.cs index e0a0bf9edf8..d8f29f20071 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientOptionsProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientOptionsProvider.cs @@ -85,10 +85,13 @@ public static ClientOptionsProvider CreateClientOptionsProvider(InputClient inpu } /// - /// Determines if a client has only standard parameters (ApiVersion and Endpoint). + /// Determines if a client can share the singleton options instance. + /// Multi-service clients always need their own options type for service-specific API version properties. + /// Only optional parameters with default values that become properties on the options class + /// should trigger a separate client-specific options type. /// /// The input client to check. - /// True if the client has only standard parameters, false otherwise. + /// True if the client can share the singleton options instance, false otherwise. private static bool UseSingletonInstance(InputClient inputClient) { var rootClients = ScmCodeModelGenerator.Instance.InputLibrary.InputNamespace.RootClients; @@ -98,6 +101,12 @@ private static bool UseSingletonInstance(InputClient inputClient) return false; } + // Multi-service clients need their own options type for service-specific API version properties + if (inputClient.IsMultiServiceClient) + { + return false; + } + foreach (var parameter in inputClient.Parameters) { // Check if parameter is NOT an ApiVersion or Endpoint parameter @@ -111,11 +120,15 @@ private static bool UseSingletonInstance(InputClient inputClient) return false; // Found a non-standard endpoint parameter } } - else + else if (parameter.DefaultValue != null) { - // Found a non-ApiVersion, non-Endpoint parameter + // Found a non-ApiVersion, non-Endpoint optional parameter that will become + // a property on the options class — requires a separate client-specific options type. return false; } + // Required parameters (DefaultValue == null) are inlined as constructor parameters + // on the client and do not become properties on the options class, + // so they should not trigger a separate client-specific options type. } } return true; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientOptionsProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientOptionsProviderTests.cs index 93664c6310b..7372b6e8f23 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientOptionsProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientOptionsProviderTests.cs @@ -466,6 +466,42 @@ public void MultipleClientsWithCustomParametersCreateSeparateOptions() Assert.AreEqual("SampleClientOptions", options2!.Name); } + [Test] + public void MultipleClientsWithRequiredCustomParametersShareSingletonOptions() + { + // Required parameters (no DefaultValue) should NOT trigger a separate client-specific options type. + // They are inlined as constructor parameters on the client, not as properties on ClientOptions. + var requiredParam = InputFactory.MethodParameter( + "knowledgeBaseName", + InputPrimitiveType.String, + isRequired: true, + scope: InputParameterScope.Client); + + var client1 = InputFactory.Client("KnowledgeBaseRetrievalClient", clientNamespace: "TestNamespace", parameters: [requiredParam]); + var client2 = InputFactory.Client("SearchClient", clientNamespace: "TestNamespace"); + + MockHelpers.LoadMockGenerator(clients: () => [client1, client2]); + + var clientProvider1 = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(client1); + var clientProvider2 = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(client2); + + Assert.IsNotNull(clientProvider1); + Assert.IsNotNull(clientProvider2); + + var options1 = clientProvider1!.ClientOptions; + var options2 = clientProvider2!.ClientOptions; + + Assert.IsNotNull(options1); + Assert.IsNotNull(options2); + + // Both clients should share the same singleton ClientOptions instance + // because the required parameter does not become a property on the options class + Assert.AreSame(options1, options2); + + // The name should be based on the InputNamespace (singleton naming) + Assert.AreEqual("SampleClientOptions", options1!.Name); + } + [Test] public void NamespaceLastSegmentIsUsedForSingletonName() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs index c18cf79e9ff..4af2c5add7f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs @@ -6,6 +6,7 @@ using System.ClientModel.Primitives; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -69,6 +70,10 @@ public bool ValidateIsLastNamespaceSegmentTheSame(string left, string right) [SetUp] public void SetUp() { + // Reset the singleton instance before each test to prevent state leaking + var singletonField = typeof(ClientOptionsProvider).GetField("_singletonInstance", BindingFlags.Static | BindingFlags.NonPublic); + singletonField?.SetValue(null, null); + var categories = TestContext.CurrentContext.Test?.Properties["Category"]; _containsSubClients = categories?.Contains(SubClientsCategory) ?? false; _hasKeyAuth = categories?.Contains(KeyAuthCategory) ?? false;