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;