From 320ee0ea1fbece15ac056472bbd99c7be06e3cf6 Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 1 Apr 2026 15:29:25 -0700 Subject: [PATCH 1/4] Include custom code properties and constructor params in ConfigurationSchema.json The ConfigurationSchemaGenerator now includes properties from CustomCodeView on client options types, and custom constructor parameters from client types. Previously, only properties from the TypeSpec input model were included in the generated schema. Hand-written properties added via partial classes (e.g., Audience on ConfigurationClientOptions) were silently dropped when the schema was regenerated. Changes: - BuildOptionsSchema: merges CustomCodeView properties with generated ones - BuildClientEntry: discovers custom constructor parameters from CustomCodeView - Added tests for both scenarios with custom code compilation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/ConfigurationSchemaGenerator.cs | 39 +++++++++ .../test/ConfigurationSchemaGeneratorTests.cs | 80 +++++++++++++++++++ .../TestServiceOptions.cs | 11 +++ .../TestService.cs | 9 +++ 4 files changed, 139 insertions(+) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestData/ConfigurationSchemaGeneratorTests/Generate_IncludesCustomCodeOptionsProperties/TestServiceOptions.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestData/ConfigurationSchemaGeneratorTests/Generate_IncludesCustomConstructorParameters/TestService.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ConfigurationSchemaGenerator.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ConfigurationSchemaGenerator.cs index 72245c0305a..363eb154def 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ConfigurationSchemaGenerator.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ConfigurationSchemaGenerator.cs @@ -115,6 +115,28 @@ private static JsonObject BuildClientEntry(ClientProvider client, string options properties[propName] = GetJsonSchemaForType(param.Type, localDefinitions); } + // Add custom constructor parameters from custom code (e.g., hand-written constructors + // added via partial classes) that are not already covered by generated parameters. + var customConstructors = client.CustomCodeView?.Constructors; + if (customConstructors != null) + { + var knownProps = new HashSet(properties.Select(p => p.Key)); + knownProps.Add("Credential"); + knownProps.Add("Options"); + foreach (var ctor in customConstructors) + { + foreach (var param in ctor.Signature.Parameters) + { + var propName = param.Name.ToIdentifierName(); + if (!knownProps.Contains(propName)) + { + properties[propName] = GetJsonSchemaForType(param.Type, localDefinitions); + knownProps.Add(propName); + } + } + } + } + // Add credential reference (defined in System.ClientModel base schema) properties["Credential"] = new JsonObject { @@ -158,6 +180,23 @@ private static JsonObject BuildOptionsSchema(ClientProvider client, string optio .Where(p => p.Modifiers.HasFlag(MethodSignatureModifiers.Public)) .ToList(); + // Also include custom code properties (e.g., hand-written properties added via partial classes) + // that are not already in the generated properties set. + var generatedPropNames = new HashSet(customProperties.Select(p => p.Name)); + var customCodeProperties = clientOptions.CustomCodeView?.Properties; + if (customCodeProperties != null) + { + foreach (var prop in customCodeProperties) + { + if (prop.Modifiers.HasFlag(MethodSignatureModifiers.Public) && + !generatedPropNames.Contains(prop.Name)) + { + customProperties.Add(prop); + generatedPropNames.Add(prop.Name); + } + } + } + var allOfArray = new JsonArray { new JsonObject { ["$ref"] = $"#/definitions/{optionsRef}" } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ConfigurationSchemaGeneratorTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ConfigurationSchemaGeneratorTests.cs index dbc47ede7e8..1f65ccbbb10 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ConfigurationSchemaGeneratorTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ConfigurationSchemaGeneratorTests.cs @@ -9,6 +9,7 @@ using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Nodes; +using System.Threading.Tasks; using Microsoft.TypeSpec.Generator.ClientModel.Providers; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Primitives; @@ -839,6 +840,85 @@ public void ConfigurationSchemaOptions_HasCorrectDefaults() Assert.IsTrue(options.GenerateNuGetTargets); } + [Test] + public async Task Generate_IncludesCustomCodeOptionsProperties() + { + // Reset singleton before loading async mock + var singletonField = typeof(ClientOptionsProvider).GetField("_singletonInstance", BindingFlags.Static | BindingFlags.NonPublic); + singletonField?.SetValue(null, null); + + // Load mock generator with a compilation that contains a custom partial class + // for TestServiceOptions with an "Audience" property. + await MockHelpers.LoadMockGeneratorAsync( + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var client = InputFactory.Client("TestService"); + var clientProvider = new ClientProvider(client); + + Assert.IsNotNull(clientProvider.ClientOptions, "ClientOptions should not be null"); + Assert.IsNotNull(clientProvider.ClientOptions!.CustomCodeView, + "CustomCodeView should be available from the compilation"); + + var output = new TestOutputLibrary([clientProvider]); + var result = ConfigurationSchemaGenerator.Generate(output); + + Assert.IsNotNull(result); + var doc = JsonNode.Parse(result!)!; + + // Find the options definition + var clientEntry = doc["properties"]?["Clients"]?["properties"]?["TestService"]; + var optionsRef = clientEntry?["properties"]?["Options"]?["$ref"]?.GetValue(); + Assert.IsNotNull(optionsRef, "Options should reference a local definition"); + var defName = optionsRef!.Replace("#/definitions/", ""); + + var optionsDef = doc["definitions"]?[defName]; + Assert.IsNotNull(optionsDef, $"Options definition '{defName}' should exist"); + + var allOf = optionsDef!["allOf"]!.AsArray(); + Assert.AreEqual(2, allOf.Count, "allOf should have base options + extension with custom properties"); + + // Verify the custom "Audience" property from the partial class is included + var extensionProperties = allOf[1]?["properties"]; + Assert.IsNotNull(extensionProperties, "Extension properties should exist"); + var audienceProp = extensionProperties!["Audience"]; + Assert.IsNotNull(audienceProp, "Custom code 'Audience' property should be included in the schema"); + Assert.AreEqual("string", audienceProp!["type"]?.GetValue()); + } + + [Test] + public async Task Generate_IncludesCustomConstructorParameters() + { + // Reset singleton before loading async mock + var singletonField = typeof(ClientOptionsProvider).GetField("_singletonInstance", BindingFlags.Static | BindingFlags.NonPublic); + singletonField?.SetValue(null, null); + + // Load mock generator with a compilation that contains a custom partial class + // for TestService with a constructor that takes a "connectionString" parameter. + await MockHelpers.LoadMockGeneratorAsync( + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var client = InputFactory.Client("TestService"); + var clientProvider = new ClientProvider(client); + + Assert.IsNotNull(clientProvider.CustomCodeView, + "CustomCodeView should be available from the compilation"); + + var output = new TestOutputLibrary([clientProvider]); + var result = ConfigurationSchemaGenerator.Generate(output); + + Assert.IsNotNull(result); + var doc = JsonNode.Parse(result!)!; + + // Verify the custom "ConnectionString" constructor parameter appears as a client-level property + var clientEntry = doc["properties"]?["Clients"]?["properties"]?["TestService"]; + Assert.IsNotNull(clientEntry, "TestService client entry should exist"); + + var connectionStringProp = clientEntry!["properties"]?["ConnectionString"]; + Assert.IsNotNull(connectionStringProp, + "Custom constructor parameter 'connectionString' should appear as 'ConnectionString' in the schema"); + Assert.AreEqual("string", connectionStringProp!["type"]?.GetValue()); + } + /// /// Test output library that wraps provided TypeProviders. /// diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestData/ConfigurationSchemaGeneratorTests/Generate_IncludesCustomCodeOptionsProperties/TestServiceOptions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestData/ConfigurationSchemaGeneratorTests/Generate_IncludesCustomCodeOptionsProperties/TestServiceOptions.cs new file mode 100644 index 00000000000..f420dba0474 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestData/ConfigurationSchemaGeneratorTests/Generate_IncludesCustomCodeOptionsProperties/TestServiceOptions.cs @@ -0,0 +1,11 @@ +#nullable disable + +using System.ClientModel.Primitives; + +namespace Sample +{ + public partial class TestServiceOptions : ClientPipelineOptions + { + public string Audience { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestData/ConfigurationSchemaGeneratorTests/Generate_IncludesCustomConstructorParameters/TestService.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestData/ConfigurationSchemaGeneratorTests/Generate_IncludesCustomConstructorParameters/TestService.cs new file mode 100644 index 00000000000..22eada52e54 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestData/ConfigurationSchemaGeneratorTests/Generate_IncludesCustomConstructorParameters/TestService.cs @@ -0,0 +1,9 @@ +#nullable disable + +namespace Sample +{ + public partial class TestService + { + public TestService(string connectionString) { } + } +} From 0fd037c79d348ca4a3485ca7e80339569a45e1bf Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 1 Apr 2026 15:33:47 -0700 Subject: [PATCH 2/4] Also bind custom code properties in ClientSettings and ClientOptions constructors Apply the same custom code property discovery to: - ClientOptionsProvider.BuildConfigurationSectionConstructor: binds custom code properties from configuration section - ClientSettingsProvider.BuildProperties: includes custom constructor params as settings properties - ClientSettingsProvider.BuildMethods (BindCore): binds custom constructor params from configuration section Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Providers/ClientOptionsProvider.cs | 25 ++++++++ .../src/Providers/ClientSettingsProvider.cs | 57 +++++++++++++++++++ 2 files changed, 82 insertions(+) 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 a2420740832..e0a0bf9edf8 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 @@ -387,6 +387,31 @@ private ConstructorProvider BuildConfigurationSectionConstructor() property.Type); } + // Also bind custom code properties (e.g., hand-written properties added via partial classes) + var generatedPropNames = new HashSet(Properties.Select(p => p.Name)); + if (versionPropertyNames != null) + { + generatedPropNames.UnionWith(versionPropertyNames); + } + var customCodeProperties = CustomCodeView?.Properties; + if (customCodeProperties != null) + { + foreach (var prop in customCodeProperties) + { + if (prop.Modifiers.HasFlag(MethodSignatureModifiers.Public) && + !generatedPropNames.Contains(prop.Name)) + { + ClientSettingsProvider.AppendBindingForProperty( + body, + sectionParam, + prop.Name, + prop.Name.ToVariableName(), + prop.Type); + generatedPropNames.Add(prop.Name); + } + } + } + return new ConstructorProvider( new ConstructorSignature( Type, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientSettingsProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientSettingsProvider.cs index c8408170637..b68a2ced595 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientSettingsProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientSettingsProvider.cs @@ -105,6 +105,34 @@ protected override PropertyProvider[] BuildProperties() this)); } + // Include custom constructor parameters from custom code (e.g., hand-written constructors + // added via partial classes) that are not already covered by generated parameters. + var knownProps = new HashSet(properties.Select(p => p.Name)); + knownProps.Add("Credential"); + knownProps.Add("Options"); + var customConstructors = _clientProvider.CustomCodeView?.Constructors; + if (customConstructors != null) + { + foreach (var ctor in customConstructors) + { + foreach (var param in ctor.Signature.Parameters) + { + var propName = param.Name.ToIdentifierName(); + if (!knownProps.Contains(propName)) + { + properties.Add(new PropertyProvider( + null, + MethodSignatureModifiers.Public, + param.Type.WithNullable(true), + propName, + new AutoPropertyBody(true), + this)); + knownProps.Add(propName); + } + } + } + } + var clientOptions = _clientProvider.EffectiveClientOptions; if (clientOptions != null) { @@ -136,6 +164,35 @@ protected override MethodProvider[] BuildMethods() AppendBindingForProperty(body, sectionParam, propName, param.Name.ToVariableName(), param.Type); } + // Bind custom constructor parameters from custom code + var knownProps = new HashSet(); + if (EndpointProperty != null) + { + knownProps.Add(EndpointProperty.Name); + } + foreach (var param in OtherRequiredParams) + { + knownProps.Add(param.Name.ToIdentifierName()); + } + knownProps.Add("Credential"); + knownProps.Add("Options"); + var customConstructors = _clientProvider.CustomCodeView?.Constructors; + if (customConstructors != null) + { + foreach (var ctor in customConstructors) + { + foreach (var param in ctor.Signature.Parameters) + { + var propName = param.Name.ToIdentifierName(); + if (!knownProps.Contains(propName)) + { + AppendBindingForProperty(body, sectionParam, propName, param.Name.ToVariableName(), param.Type); + knownProps.Add(propName); + } + } + } + } + var clientOptions = _clientProvider.EffectiveClientOptions; if (clientOptions != null) { From 7060fd4a75fedeed7bf5fd6b14cb10c8c30b1d3b Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 1 Apr 2026 15:40:08 -0700 Subject: [PATCH 3/4] Add tests for ClientSettings and ClientOptions custom code binding - ClientOptionsProviderTests: verify config section constructor binds custom code properties (e.g., Audience from partial class) - ClientSettingsProviderTests: verify custom constructor parameters appear in settings properties and BindCore method Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Providers/ClientOptionsProviderTests.cs | 23 ++++++++ .../Providers/ClientSettingsProviderTests.cs | 52 +++++++++++++++++++ .../TestClientOptions.cs | 11 ++++ .../TestClient.cs | 9 ++++ .../TestClient.cs | 9 ++++ 5 files changed, 104 insertions(+) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/TestConfigurationSectionConstructorBody_BindsCustomCodeProperties/TestClientOptions.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestBindCoreMethod_BindsCustomConstructorParameters/TestClient.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestProperties_IncludesCustomConstructorParameters/TestClient.cs 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 63d5bb33f97..93664c6310b 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 @@ -1120,5 +1120,28 @@ public void TestConfigurationSectionConstructorBody_WithFixedEnumProperty() Assert.IsFalse(bodyString.Contains("new ClientMode"), "IConfigurationSection constructor should NOT use new for fixed enum property binding"); } + + [Test] + public async Task TestConfigurationSectionConstructorBody_BindsCustomCodeProperties() + { + await MockHelpers.LoadMockGeneratorAsync( + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var client = InputFactory.Client("TestClient", clientNamespace: "SampleNamespace"); + var clientProvider = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(client); + var clientOptionsProvider = clientProvider!.ClientOptions; + + Assert.IsNotNull(clientOptionsProvider); + Assert.IsNotNull(clientOptionsProvider!.CustomCodeView, + "CustomCodeView should be available from the compilation"); + + var configSectionCtor = clientOptionsProvider.Constructors + .FirstOrDefault(c => c.Signature.Parameters.Any(p => p.Name == "section")); + Assert.IsNotNull(configSectionCtor); + + var bodyString = configSectionCtor!.BodyStatements!.ToDisplayString(); + Assert.IsTrue(bodyString.Contains("Audience"), + "IConfigurationSection constructor should bind the custom code 'Audience' property from configuration"); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientSettingsProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientSettingsProviderTests.cs index d864e4b3ea9..b0bde02be11 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientSettingsProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientSettingsProviderTests.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Threading.Tasks; using Microsoft.TypeSpec.Generator.ClientModel.Providers; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Primitives; @@ -912,5 +913,56 @@ private static bool IsSettingsConstructor(ConstructorProvider c) => c.Signature?.Initializer != null && c.Signature?.Modifiers == MethodSignatureModifiers.Public && c.Signature.Parameters.Any(p => p.Name == "settings"); + + [Test] + public async Task TestProperties_IncludesCustomConstructorParameters() + { + var singletonField = typeof(ClientOptionsProvider).GetField("_singletonInstance", + System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic); + singletonField?.SetValue(null, null); + + await MockHelpers.LoadMockGeneratorAsync( + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var client = InputFactory.Client("TestClient", clientNamespace: "SampleNamespace"); + var clientProvider = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(client); + Assert.IsNotNull(clientProvider); + Assert.IsNotNull(clientProvider!.CustomCodeView, + "CustomCodeView should be available from the compilation"); + + var settings = clientProvider.ClientSettings; + Assert.IsNotNull(settings); + + var connectionStringProp = settings!.Properties + .FirstOrDefault(p => p.Name == "ConnectionString"); + Assert.IsNotNull(connectionStringProp, + "Settings should include 'ConnectionString' property from custom constructor parameter"); + } + + [Test] + public async Task TestBindCoreMethod_BindsCustomConstructorParameters() + { + var singletonField = typeof(ClientOptionsProvider).GetField("_singletonInstance", + System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic); + singletonField?.SetValue(null, null); + + await MockHelpers.LoadMockGeneratorAsync( + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var client = InputFactory.Client("TestClient", clientNamespace: "SampleNamespace"); + var clientProvider = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(client); + Assert.IsNotNull(clientProvider); + + var settings = clientProvider!.ClientSettings; + Assert.IsNotNull(settings); + + var bindCoreMethod = settings!.Methods + .FirstOrDefault(m => m.Signature.Name == "BindCore"); + Assert.IsNotNull(bindCoreMethod); + + var bodyString = bindCoreMethod!.BodyStatements!.ToDisplayString(); + Assert.IsTrue(bodyString.Contains("ConnectionString"), + "BindCore should bind the custom constructor parameter 'ConnectionString' from configuration"); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/TestConfigurationSectionConstructorBody_BindsCustomCodeProperties/TestClientOptions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/TestConfigurationSectionConstructorBody_BindsCustomCodeProperties/TestClientOptions.cs new file mode 100644 index 00000000000..30f1955a770 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/TestConfigurationSectionConstructorBody_BindsCustomCodeProperties/TestClientOptions.cs @@ -0,0 +1,11 @@ +#nullable disable + +using System.ClientModel.Primitives; + +namespace SampleNamespace +{ + public partial class TestClientOptions : ClientPipelineOptions + { + public string Audience { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestBindCoreMethod_BindsCustomConstructorParameters/TestClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestBindCoreMethod_BindsCustomConstructorParameters/TestClient.cs new file mode 100644 index 00000000000..488eb113e09 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestBindCoreMethod_BindsCustomConstructorParameters/TestClient.cs @@ -0,0 +1,9 @@ +#nullable disable + +namespace SampleNamespace +{ + public partial class TestClient + { + public TestClient(string connectionString) { } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestProperties_IncludesCustomConstructorParameters/TestClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestProperties_IncludesCustomConstructorParameters/TestClient.cs new file mode 100644 index 00000000000..488eb113e09 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestProperties_IncludesCustomConstructorParameters/TestClient.cs @@ -0,0 +1,9 @@ +#nullable disable + +namespace SampleNamespace +{ + public partial class TestClient + { + public TestClient(string connectionString) { } + } +} From d0464ab25a1bf0d32d6ee223f3010c5ca0f10ef0 Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 1 Apr 2026 16:21:49 -0700 Subject: [PATCH 4/4] Address PR feedback: avoid allocation when no custom ctors; filter standard types - Move knownProps HashSet allocation inside the custom constructors null check to avoid unnecessary allocation (PR feedback from jorgerangel-msft) - Add IsStandardParameterType helper to filter out credential types (AuthenticationTokenProvider), endpoint types (Uri), and options types from custom constructor parameter discovery - Compare by both type equality and name to handle Roslyn vs typeof CSharpType mismatch Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/ConfigurationSchemaGenerator.cs | 4 +- .../src/Providers/ClientSettingsProvider.cs | 70 ++++++++++++++----- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ConfigurationSchemaGenerator.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ConfigurationSchemaGenerator.cs index 363eb154def..fbe69ae179f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ConfigurationSchemaGenerator.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ConfigurationSchemaGenerator.cs @@ -117,6 +117,7 @@ private static JsonObject BuildClientEntry(ClientProvider client, string options // Add custom constructor parameters from custom code (e.g., hand-written constructors // added via partial classes) that are not already covered by generated parameters. + // Skip credential types, endpoint types (Uri), and options types as they are handled separately. var customConstructors = client.CustomCodeView?.Constructors; if (customConstructors != null) { @@ -128,7 +129,8 @@ private static JsonObject BuildClientEntry(ClientProvider client, string options foreach (var param in ctor.Signature.Parameters) { var propName = param.Name.ToIdentifierName(); - if (!knownProps.Contains(propName)) + if (!knownProps.Contains(propName) && + !ClientSettingsProvider.IsStandardParameterType(param.Type)) { properties[propName] = GetJsonSchemaForType(param.Type, localDefinitions); knownProps.Add(propName); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientSettingsProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientSettingsProvider.cs index b68a2ced595..a3b06754522 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientSettingsProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientSettingsProvider.cs @@ -107,18 +107,19 @@ protected override PropertyProvider[] BuildProperties() // Include custom constructor parameters from custom code (e.g., hand-written constructors // added via partial classes) that are not already covered by generated parameters. - var knownProps = new HashSet(properties.Select(p => p.Name)); - knownProps.Add("Credential"); - knownProps.Add("Options"); + // Skip credential types, endpoint types (Uri), and options types as they are handled separately. var customConstructors = _clientProvider.CustomCodeView?.Constructors; if (customConstructors != null) { + var knownProps = new HashSet(properties.Select(p => p.Name)); + knownProps.Add("Credential"); + knownProps.Add("Options"); foreach (var ctor in customConstructors) { foreach (var param in ctor.Signature.Parameters) { var propName = param.Name.ToIdentifierName(); - if (!knownProps.Contains(propName)) + if (!knownProps.Contains(propName) && !IsStandardParameterType(param.Type)) { properties.Add(new PropertyProvider( null, @@ -165,26 +166,27 @@ protected override MethodProvider[] BuildMethods() } // Bind custom constructor parameters from custom code - var knownProps = new HashSet(); - if (EndpointProperty != null) - { - knownProps.Add(EndpointProperty.Name); - } - foreach (var param in OtherRequiredParams) - { - knownProps.Add(param.Name.ToIdentifierName()); - } - knownProps.Add("Credential"); - knownProps.Add("Options"); + // Skip credential types, endpoint types (Uri), and options types as they are handled separately. var customConstructors = _clientProvider.CustomCodeView?.Constructors; if (customConstructors != null) { + var knownProps = new HashSet(); + if (EndpointProperty != null) + { + knownProps.Add(EndpointProperty.Name); + } + foreach (var param in OtherRequiredParams) + { + knownProps.Add(param.Name.ToIdentifierName()); + } + knownProps.Add("Credential"); + knownProps.Add("Options"); foreach (var ctor in customConstructors) { foreach (var param in ctor.Signature.Parameters) { var propName = param.Name.ToIdentifierName(); - if (!knownProps.Contains(propName)) + if (!knownProps.Contains(propName) && !IsStandardParameterType(param.Type)) { AppendBindingForProperty(body, sectionParam, propName, param.Name.ToVariableName(), param.Type); knownProps.Add(propName); @@ -448,5 +450,41 @@ internal static void AppendComplexObjectBinding( ifExistsStatement.Add(This.Property(propName).Assign(New.Instance(type, sectionVar)).Terminate()); body.Add(ifExistsStatement); } + + /// + /// Checks if a type is a standard client parameter type that should not be included as a + /// custom settings property (credential types, endpoint types, or options types). + /// + internal static bool IsStandardParameterType(CSharpType type) + { + var effectiveType = type.IsNullable ? type.WithNullable(false) : type; + + // Skip endpoint types (Uri) + if (effectiveType.IsFrameworkType && effectiveType.FrameworkType == typeof(Uri)) + { + return true; + } + + // Skip credential types — compare by both type equality and name since the CSharpType + // from CustomCodeView (Roslyn-based) may not directly equal typeof()-based CSharpType. + var tokenCredentialType = ScmCodeModelGenerator.Instance.TypeFactory.ClientPipelineApi.TokenCredentialType; + if (tokenCredentialType != null) + { + if (effectiveType.Equals(tokenCredentialType) || + effectiveType.Name == tokenCredentialType.Name) + { + return true; + } + } + + // Skip options types (derives from ClientPipelineOptions) + var optionsType = ScmCodeModelGenerator.Instance.TypeFactory.ClientPipelineApi.ClientPipelineOptionsType; + if (effectiveType.Equals(optionsType) || effectiveType.Name == optionsType.Name) + { + return true; + } + + return false; + } } }