Skip to content

Commit ed27486

Browse files
Generate ClientSettings and auth constructors for individually-initialized sub-clients (#10042)
## Summary Extends the C# generator to produce `ClientSettings` classes and authentication constructors for sub-clients that have `InitializedBy.Individually` (i.e., can be constructed directly by the user, not only via parent client accessors). ## Changes ### ClientProvider.cs - **Auth fields for individually-initialized sub-clients**: Sub-clients with `InitializedBy.Individually` now get `_apiKeyAuthFields` / `_oauth2Fields` (previously only root clients had these). - **Auth constructors for sub-clients**: `AppendSubClientPublicConstructors` now generates credential-based constructors (`ApiKeyCredential`, `AuthenticationTokenProvider`) that chain to the internal `AuthenticationPolicy` constructor via `this(...)`. - **Internal constructor always uses auth policy**: Changed `BuildPrimaryConstructorBody` condition from `authPolicyParam != null && authFields != null` to `authPolicyParam != null`. - **`EffectiveClientOptions` property**: Sub-clients resolve the root client's `ClientOptions` type via `GetRootClient()?.ClientOptions`. - **Removed auth params from parent-init constructor**: `GetSubClientInternalConstructorParameters` no longer includes credential params. ### ClientSettingsProvider.cs - Uses `_clientProvider.EffectiveClientOptions` instead of duplicating parent-walking logic. ### ScmOutputLibrary.cs - `ClientSettings` emission decoupled from `ClientOptions != null`. ### Test Updates - Updated 8 expected test data files for the pipeline body change. - Updated sub-client constructor test assertions for the new auth constructor pattern. - 6 new sub-client settings tests in `ClientSettingsProviderTests.cs`. ### Generated Output - `Metrics.cs` now has auth constructors (`ApiKeyCredential`, `AuthenticationTokenProvider`) and auth pipeline. - `MetricsSettings.cs` — new generated settings class for the Metrics sub-client. ## Testing - All 1220 unit tests pass - `Generate.ps1` succeeds with no diff beyond expected changes --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent fd31020 commit ed27486

27 files changed

Lines changed: 610 additions & 126 deletions

File tree

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs

Lines changed: 104 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,10 @@ public ClientProvider(InputClient inputClient)
106106
_publicCtorDescription = $"Initializes a new instance of {Name}.";
107107
ClientOptions = _inputClient.Parent is null ? ClientOptionsProvider.CreateClientOptionsProvider(_inputClient, this) : null;
108108
ClientOptionsParameter = ClientOptions != null ? ScmKnownParameters.ClientOptions(ClientOptions.Type) : null;
109-
ClientSettings = ClientOptions != null ? new ClientSettingsProvider(_inputClient, this) : null;
109+
bool isIndividuallyInitialized = (_inputClient.InitializedBy & InputClientInitializedBy.Individually) != 0;
110+
ClientSettings = isIndividuallyInitialized
111+
? new ClientSettingsProvider(_inputClient, this)
112+
: null;
110113
IsMultiServiceClient = _inputClient.IsMultiServiceClient;
111114

112115
var apiKey = _inputAuth?.ApiKey;
@@ -133,8 +136,7 @@ public ClientProvider(InputClient inputClient)
133136
this,
134137
initializationValue: Literal(apiKey.Prefix)) :
135138
null;
136-
// skip auth fields for sub-clients
137-
_apiKeyAuthFields = ClientOptions is null ? null : new(apiKeyAuthField, authorizationHeaderField, authorizationApiKeyPrefixField);
139+
_apiKeyAuthFields = isIndividuallyInitialized ? new(apiKeyAuthField, authorizationHeaderField, authorizationApiKeyPrefixField) : null;
138140
}
139141

140142
var tokenAuth = _inputAuth?.OAuth2;
@@ -158,8 +160,7 @@ public ClientProvider(InputClient inputClient)
158160

159161
var tokenCredentialScopesField = BuildTokenCredentialScopesField(tokenAuth, tokenCredentialType);
160162

161-
// skip auth fields for sub-clients
162-
_oauth2Fields = ClientOptions is null ? null : new(tokenCredentialField, tokenCredentialScopesField);
163+
_oauth2Fields = isIndividuallyInitialized ? new(tokenCredentialField, tokenCredentialScopesField) : null;
163164
}
164165
EndpointField = new(
165166
FieldModifiers.Private | FieldModifiers.ReadOnly,
@@ -300,14 +301,8 @@ private IReadOnlyList<ParameterProvider> GetSubClientInternalConstructorParamete
300301
PipelineProperty.AsParameter
301302
};
302303

303-
if (_apiKeyAuthFields != null)
304-
{
305-
subClientParameters.Add(_apiKeyAuthFields.AuthField.AsParameter);
306-
}
307-
if (_oauth2Fields != null)
308-
{
309-
subClientParameters.Add(_oauth2Fields.AuthField.AsParameter);
310-
}
304+
// Auth credentials are NOT included here — the parent passes its authenticated
305+
// pipeline, so the sub-client doesn't need separate credential parameters.
311306
subClientParameters.Add(_endpointParameter);
312307
subClientParameters.AddRange(ClientParameters);
313308

@@ -385,6 +380,12 @@ private IReadOnlyList<ParameterProvider> GetClientParameters()
385380
public ClientOptionsProvider? ClientOptions { get; }
386381
public ClientSettingsProvider? ClientSettings { get; }
387382

383+
/// <summary>
384+
/// Gets the effective <see cref="ClientOptionsProvider"/> — the client's own options for root clients,
385+
/// or the root client's options for individually-initialized sub-clients.
386+
/// </summary>
387+
internal ClientOptionsProvider? EffectiveClientOptions => ClientOptions ?? GetRootClient()?.ClientOptions;
388+
388389
public PropertyProvider PipelineProperty { get; }
389390
public FieldProvider EndpointField { get; }
390391

@@ -647,7 +648,9 @@ void AppendPublicConstructors(
647648
foreach (var p in requiredParameters)
648649
{
649650
if (authParamName == null || p.Name != authParamName)
651+
{
650652
initializerArgs.Add(p);
653+
}
651654
}
652655
initializerArgs.Add(ClientOptionsParameter!);
653656

@@ -686,6 +689,14 @@ private IEnumerable<ConstructorProvider> BuildSettingsConstructors()
686689
yield break;
687690
}
688691

692+
// Only publicly constructible clients should get the Settings constructor.
693+
// Internal clients (e.g., those made internal via custom code) cannot be
694+
// constructed by consumers, so a public Settings constructor is not useful.
695+
if (!DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public))
696+
{
697+
yield break;
698+
}
699+
689700
var settingsParam = new ParameterProvider(SettingsParamName, $"The settings for {Name}.", ClientSettings.Type);
690701
var experimentalAttr = new AttributeStatement(typeof(ExperimentalAttribute), [Literal(ClientSettingsProvider.ClientSettingsDiagnosticId)]);
691702

@@ -733,64 +744,108 @@ private IEnumerable<ConstructorProvider> BuildSettingsConstructors()
733744
private void AppendSubClientPublicConstructors(List<ConstructorProvider> constructors)
734745
{
735746
// For sub-clients that can be initialized individually, we need to create public constructors
736-
// similar to the root client constructors but adapted for sub-client needs
747+
// with the same auth pattern as the root client.
737748
var primaryConstructors = new List<ConstructorProvider>();
738749
var secondaryConstructors = new List<ConstructorProvider>();
739750

740-
// if there is key auth
751+
var rootClient = GetRootClient();
752+
var clientOptionsParameter = rootClient?.ClientOptionsParameter;
753+
var clientOptionsProvider = rootClient?.ClientOptions;
754+
755+
if (clientOptionsParameter == null || clientOptionsProvider == null)
756+
{
757+
return;
758+
}
759+
760+
// Add the internal AuthenticationPolicy constructor first — public constructors chain to it.
761+
var authPolicyParam = new ParameterProvider(
762+
"authenticationPolicy",
763+
$"The authentication policy to use for pipeline creation.",
764+
new CSharpType(typeof(AuthenticationPolicy), isNullable: true));
765+
766+
var requiredNonAuthParams = GetRequiredParameters(null);
767+
ParameterProvider[] internalConstructorParameters = [authPolicyParam, _endpointParameter, .. requiredNonAuthParams, clientOptionsParameter];
768+
769+
var internalConstructor = new ConstructorProvider(
770+
new ConstructorSignature(Type, _publicCtorDescription, MethodSignatureModifiers.Internal, internalConstructorParameters),
771+
BuildPrimaryConstructorBody(internalConstructorParameters, null, authPolicyParam, clientOptionsProvider, clientOptionsParameter, addExplicitValidation: true),
772+
this);
773+
primaryConstructors.Add(internalConstructor);
774+
775+
// Add public constructors with auth — same pattern as root client
741776
if (_apiKeyAuthFields != null)
742777
{
743778
AppendSubClientPublicConstructorsForAuth(_apiKeyAuthFields, primaryConstructors, secondaryConstructors);
744779
}
745-
// if there is oauth2 auth
746780
if (_oauth2Fields != null)
747781
{
748782
AppendSubClientPublicConstructorsForAuth(_oauth2Fields, primaryConstructors, secondaryConstructors);
749783
}
750784

751-
// if there is no auth
785+
bool onlyContainsUnsupportedAuth = _inputAuth != null && _apiKeyAuthFields == null && _oauth2Fields == null;
752786
if (_apiKeyAuthFields == null && _oauth2Fields == null)
753787
{
754-
AppendSubClientPublicConstructorsForAuth(null, primaryConstructors, secondaryConstructors);
788+
AppendSubClientPublicConstructorsForAuth(null, primaryConstructors, secondaryConstructors, onlyContainsUnsupportedAuth);
755789
}
756790

757791
constructors.AddRange(secondaryConstructors);
758792
constructors.AddRange(primaryConstructors);
759793

794+
// Add Settings constructor for individually-initialized sub-clients
795+
foreach (var settingsConstructor in BuildSettingsConstructors())
796+
{
797+
constructors.Add(settingsConstructor);
798+
}
799+
760800
void AppendSubClientPublicConstructorsForAuth(
761801
AuthFields? authFields,
762802
List<ConstructorProvider> primaryConstructors,
763-
List<ConstructorProvider> secondaryConstructors)
764-
{
765-
// For a sub-client with individual initialization, we need:
766-
// - endpoint parameter
767-
// - auth parameter (if auth exists)
768-
// - client options parameter (we need to get this from the root client)
769-
var rootClient = GetRootClient();
770-
var clientOptionsParameter = rootClient?.ClientOptionsParameter;
771-
var clientOptionsProvider = rootClient?.ClientOptions;
772-
if (clientOptionsParameter == null || clientOptionsProvider == null)
803+
List<ConstructorProvider> secondaryConstructors,
804+
bool onlyContainsUnsupportedAuth = false)
805+
{
806+
// Public constructor with credential parameter — delegates to the internal constructor via this(...).
807+
var requiredParameters = GetRequiredParameters(authFields?.AuthField);
808+
ParameterProvider[] primaryConstructorParameters = [_endpointParameter, .. requiredParameters, clientOptionsParameter];
809+
var constructorModifier = onlyContainsUnsupportedAuth ? MethodSignatureModifiers.Internal : MethodSignatureModifiers.Public;
810+
811+
// Build the auth policy expression for the this() initializer
812+
ValueExpression authPolicyArg = BuildAuthPolicyArgument(authFields, requiredParameters);
813+
var initializerArgs = new List<ValueExpression> { authPolicyArg, _endpointParameter };
814+
string? authParamName = authFields != null
815+
? (authFields.AuthField.Name != TokenProviderFieldName ? CredentialParamName : authFields.AuthField.AsParameter.Name)
816+
: null;
817+
foreach (var p in requiredParameters)
773818
{
774-
// Cannot create public constructor without client options
775-
return;
819+
if (authParamName == null || p.Name != authParamName)
820+
{
821+
initializerArgs.Add(p);
822+
}
776823
}
824+
initializerArgs.Add(clientOptionsParameter!);
777825

778-
var requiredParameters = GetRequiredParameters(authFields?.AuthField);
779-
ParameterProvider[] primaryConstructorParameters = [_endpointParameter, .. requiredParameters, clientOptionsParameter];
780826
var primaryConstructor = new ConstructorProvider(
781-
new ConstructorSignature(Type, _publicCtorDescription, MethodSignatureModifiers.Public, primaryConstructorParameters),
782-
BuildPrimaryConstructorBody(primaryConstructorParameters, authFields, null, clientOptionsProvider, clientOptionsParameter),
827+
new ConstructorSignature(Type, _publicCtorDescription, constructorModifier, primaryConstructorParameters,
828+
initializer: new ConstructorInitializer(false, initializerArgs)),
829+
MethodBodyStatement.Empty,
783830
this);
784-
785831
primaryConstructors.Add(primaryConstructor);
786832

787833
// If the endpoint parameter contains an initialization value, it is not required.
788834
ParameterProvider[] secondaryConstructorParameters = _endpointParameter.InitializationValue is null
789835
? [_endpointParameter, .. requiredParameters]
790836
: [.. requiredParameters];
791-
var secondaryConstructor = BuildSecondaryConstructor(secondaryConstructorParameters, primaryConstructorParameters, MethodSignatureModifiers.Public);
837+
var secondaryConstructor = BuildSecondaryConstructor(secondaryConstructorParameters, primaryConstructorParameters, constructorModifier);
792838

793839
secondaryConstructors.Add(secondaryConstructor);
840+
841+
// When endpoint has a default value and there are required parameters,
842+
// add an additional constructor that accepts required parameters + options.
843+
if (_endpointParameter.InitializationValue is not null && requiredParameters.Count > 0)
844+
{
845+
ParameterProvider[] simplifiedConstructorWithOptionsParameters = [.. requiredParameters, clientOptionsParameter];
846+
var simplifiedConstructorWithOptions = BuildSecondaryConstructor(simplifiedConstructorWithOptionsParameters, primaryConstructorParameters, constructorModifier);
847+
secondaryConstructors.Add(simplifiedConstructorWithOptions);
848+
}
794849
}
795850
}
796851

@@ -917,11 +972,18 @@ private MethodBodyStatement[] BuildPrimaryConstructorBody(IReadOnlyList<Paramete
917972
}
918973

919974
ValueExpression perRetryPolicies;
920-
if (authPolicyParam != null && authFields != null)
975+
if (authPolicyParam != null)
921976
{
922-
// Internal implementation constructor: use the authenticationPolicy parameter directly
923-
perRetryPoliciesList.Add(authPolicyParam);
924-
perRetryPolicies = New.Array(ScmCodeModelGenerator.Instance.TypeFactory.ClientPipelineApi.PipelinePolicyType, isInline: true, [.. perRetryPoliciesList]);
977+
// Internal implementation constructor: generate a runtime null check for the auth policy.
978+
// No-auth clients pass null, so we must guard against adding null to the policies array.
979+
var pipelinePolicyType = ScmCodeModelGenerator.Instance.TypeFactory.ClientPipelineApi.PipelinePolicyType;
980+
var perRetryWithoutAuth = New.Array(pipelinePolicyType, isInline: true, [.. perRetryPoliciesList]);
981+
var perRetryWithAuth = New.Array(pipelinePolicyType, isInline: true, [.. perRetryPoliciesList, authPolicyParam]);
982+
983+
body.Add(new IfElseStatement(
984+
authPolicyParam.NotEqual(Null),
985+
PipelineProperty.Assign(This.ToApi<ClientPipelineApi>().Create(clientOptionsParameter, perRetryWithAuth)).Terminate(),
986+
PipelineProperty.Assign(This.ToApi<ClientPipelineApi>().Create(clientOptionsParameter, perRetryWithoutAuth)).Terminate()));
925987
}
926988
else
927989
{
@@ -940,9 +1002,9 @@ private MethodBodyStatement[] BuildPrimaryConstructorBody(IReadOnlyList<Paramete
9401002
perRetryPolicies = New.Array(ScmCodeModelGenerator.Instance.TypeFactory.ClientPipelineApi.PipelinePolicyType, isInline: true, [.. perRetryPoliciesList]);
9411003
break;
9421004
}
943-
}
9441005

945-
body.Add(PipelineProperty.Assign(This.ToApi<ClientPipelineApi>().Create(clientOptionsParameter, perRetryPolicies)).Terminate());
1006+
body.Add(PipelineProperty.Assign(This.ToApi<ClientPipelineApi>().Create(clientOptionsParameter, perRetryPolicies)).Terminate());
1007+
}
9461008

9471009
foreach (var f in Fields)
9481010
{

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientSettingsProvider.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,13 @@ protected override PropertyProvider[] BuildProperties()
9696
this));
9797
}
9898

99-
if (_clientProvider.ClientOptions != null)
99+
var clientOptions = _clientProvider.EffectiveClientOptions;
100+
if (clientOptions != null)
100101
{
101102
properties.Add(new PropertyProvider(
102103
null,
103104
MethodSignatureModifiers.Public,
104-
_clientProvider.ClientOptions.Type.WithNullable(true),
105+
clientOptions.Type.WithNullable(true),
105106
"Options",
106107
new AutoPropertyBody(true),
107108
this));
@@ -126,9 +127,10 @@ protected override MethodProvider[] BuildMethods()
126127
AppendBindingForProperty(body, sectionParam, propName, param.Name.ToVariableName(), param.Type);
127128
}
128129

129-
if (_clientProvider.ClientOptions != null)
130+
var clientOptions = _clientProvider.EffectiveClientOptions;
131+
if (clientOptions != null)
130132
{
131-
AppendComplexObjectBinding(body, sectionParam, "Options", "options", _clientProvider.ClientOptions.Type);
133+
AppendComplexObjectBinding(body, sectionParam, "Options", "options", clientOptions.Type);
132134
}
133135

134136
var bindCoreMethod = new MethodProvider(

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmOutputLibrary.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Collections.Generic;
55
using Microsoft.TypeSpec.Generator.ClientModel.Providers;
66
using Microsoft.TypeSpec.Generator.Input;
7+
using Microsoft.TypeSpec.Generator.Primitives;
78
using Microsoft.TypeSpec.Generator.Providers;
89

910
namespace Microsoft.TypeSpec.Generator.ClientModel
@@ -41,11 +42,13 @@ private static void BuildClient(InputClient inputClient, HashSet<TypeProvider> t
4142
if (clientOptions != null)
4243
{
4344
types.Add(clientOptions);
44-
var clientSettings = client.ClientSettings;
45-
if (clientSettings != null)
46-
{
47-
types.Add(clientSettings);
48-
}
45+
}
46+
47+
// Emit the Settings class for any publicly constructible client (root or individually-initialized sub-client).
48+
var clientSettings = client.ClientSettings;
49+
if (clientSettings != null && client.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public))
50+
{
51+
types.Add(clientSettings);
4952
}
5053

5154
// We use the spec view methods so that we include collection definitions even if the user is customizing or suppressing

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderCustomizationTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ public async Task CanRenameSubClient()
281281
]);
282282
var inputServiceMethod = InputFactory.BasicServiceMethod("test", inputOperation);
283283
var inputClient = InputFactory.Client("TestClient", methods: [inputServiceMethod]);
284-
InputClient subClient = InputFactory.Client("custom", parent: inputClient);
284+
InputClient subClient = InputFactory.Client("custom", parent: inputClient, initializedBy: InputClientInitializedBy.Parent);
285285
var mockGenerator = await MockHelpers.LoadMockGeneratorAsync(
286286
clients: () => [inputClient],
287287
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());
@@ -309,7 +309,7 @@ public async Task CanRemoveCachingField()
309309
]);
310310
var inputServiceMethod = InputFactory.BasicServiceMethod("test", inputOperation);
311311
var inputClient = InputFactory.Client("TestClient", methods: [inputServiceMethod]);
312-
InputClient subClient = InputFactory.Client("dog", methods: [], parameters: [], parent: inputClient);
312+
InputClient subClient = InputFactory.Client("dog", methods: [], parameters: [], parent: inputClient, initializedBy: InputClientInitializedBy.Parent);
313313
var mockGenerator = await MockHelpers.LoadMockGeneratorAsync(
314314
clients: () => [inputClient],
315315
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

0 commit comments

Comments
 (0)