From d8091a6bff8d3e11e581d76d5ce0822435119adb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:08:45 +0000 Subject: [PATCH 1/5] Initial plan From bad59683ce9d9b1c7d0b46aadbe7f70fa0f9a601 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:57:55 +0000 Subject: [PATCH 2/5] fix: use wire type for endpoint params in ClientSettings instead of always Uri Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ClientSettingsProvider.cs | 16 +++-- .../Providers/ClientSettingsProviderTests.cs | 64 ++++++++++++++++--- 2 files changed, 68 insertions(+), 12 deletions(-) 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 06b3e7934c7..e7d5fe26d60 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 @@ -39,6 +39,11 @@ internal ClientSettingsProvider(InputClient inputClient, ClientProvider clientPr .FirstOrDefault(p => p is InputEndpointParameter ep && ep.IsEndpoint) as InputEndpointParameter; EndpointPropertyName = inputEndpointParam?.Name.ToIdentifierName(); + if (inputEndpointParam != null) + { + EndpointPropertyType = ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(inputEndpointParam.Type); + } + // Collect non-endpoint, non-apiVersion required parameters (auth params come separately via InputClient.Auth) OtherRequiredParams = inputClient.Parameters .Where(p => p.IsRequired && !p.IsApiVersion && @@ -51,6 +56,9 @@ internal ClientSettingsProvider(InputClient inputClient, ClientProvider clientPr internal string? EndpointPropertyName { get; } + /// Gets the CSharp type of the endpoint parameter (e.g. string, Uri). + internal CSharpType? EndpointPropertyType { get; } + /// Gets non-endpoint, non-auth required parameters that have settings properties. internal IReadOnlyList OtherRequiredParams { get; } @@ -74,12 +82,12 @@ protected override PropertyProvider[] BuildProperties() { var properties = new List(); - if (EndpointPropertyName != null) + if (EndpointPropertyName != null && EndpointPropertyType != null) { properties.Add(new PropertyProvider( null, MethodSignatureModifiers.Public, - new CSharpType(typeof(Uri), isNullable: true), + EndpointPropertyType.WithNullable(true), EndpointPropertyName, new AutoPropertyBody(true), this)); @@ -116,9 +124,9 @@ protected override MethodProvider[] BuildMethods() var sectionParam = new ParameterProvider("section", $"The configuration section.", IConfigurationSectionType); var body = new List(); - if (EndpointPropertyName != null) + if (EndpointPropertyName != null && EndpointPropertyType != null) { - AppendUriTryCreateBinding(body, sectionParam, EndpointPropertyName, EndpointPropertyName.ToVariableName()); + AppendBindingForProperty(body, sectionParam, EndpointPropertyName, EndpointPropertyName.ToVariableName(), EndpointPropertyType); } foreach (var param in OtherRequiredParams) 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 c4f31a8a1a0..0c7278798df 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 @@ -60,9 +60,9 @@ public void TestProperties_WithEndpoint() Assert.IsNotNull(settingsProvider); var properties = settingsProvider!.Properties; - // Should have Endpoint and Options properties - var endpointProp = properties.FirstOrDefault(p => p.Name == "Endpoint" && p.Type.Equals(new CSharpType(typeof(Uri), isNullable: true))); - Assert.IsNotNull(endpointProp, "Settings should have an Endpoint property of type Uri?"); + // String endpoint parameter should produce a string? property, not Uri? + var endpointProp = properties.FirstOrDefault(p => p.Name == "Endpoint" && p.Type.Equals(new CSharpType(typeof(string), isNullable: true))); + Assert.IsNotNull(endpointProp, "Settings should have an Endpoint property of type string?"); var optionsProp = properties.FirstOrDefault(p => p.Name == "Options"); Assert.IsNotNull(optionsProp, "Settings should have an Options property"); @@ -78,7 +78,7 @@ public void TestProperties_NoEndpoint() // Settings provider should exist but without endpoint-related properties Assert.IsNotNull(settingsProvider); - var endpointProp = settingsProvider!.Properties.FirstOrDefault(p => p.Name == "Endpoint" && p.Type.Equals(new CSharpType(typeof(Uri), isNullable: true))); + var endpointProp = settingsProvider!.Properties.FirstOrDefault(p => p.Name == "Endpoint"); Assert.IsNull(endpointProp, "Settings should not have an Endpoint property when no endpoint parameter exists"); } @@ -113,11 +113,59 @@ public void TestBindCoreMethod_WithEndpoint() Assert.AreEqual(1, bindCoreMethod.Signature.Parameters.Count); Assert.AreEqual("section", bindCoreMethod.Signature.Parameters[0].Name); - // Validate the body contains Uri.TryCreate for endpoint binding + // Validate the body uses string binding for string endpoint parameter var body = bindCoreMethod.BodyStatements; Assert.IsNotNull(body); var bodyString = body!.ToDisplayString(); - Assert.IsTrue(bodyString.Contains("TryCreate"), "BindCore should use Uri.TryCreate for endpoint binding"); + Assert.IsTrue(bodyString.Contains("IsNullOrEmpty"), "BindCore should use string.IsNullOrEmpty for string endpoint binding"); + } + + [Test] + public void TestProperties_WithUrlEndpoint() + { + var inputParameters = new[] + { + InputFactory.EndpointParameter( + "endpoint", + InputPrimitiveType.Url, + scope: InputParameterScope.Client, + isEndpoint: true) + }; + var client = InputFactory.Client("TestClient", parameters: inputParameters); + var clientProvider = new ClientProvider(client); + var settingsProvider = clientProvider.ClientSettings; + + Assert.IsNotNull(settingsProvider); + + var properties = settingsProvider!.Properties; + // Url endpoint parameter should produce a Uri? property + var endpointProp = properties.FirstOrDefault(p => p.Name == "Endpoint" && p.Type.Equals(new CSharpType(typeof(Uri), isNullable: true))); + Assert.IsNotNull(endpointProp, "Settings should have an Endpoint property of type Uri? for Url endpoint"); + } + + [Test] + public void TestBindCoreMethod_WithUrlEndpoint() + { + var inputParameters = new[] + { + InputFactory.EndpointParameter( + "endpoint", + InputPrimitiveType.Url, + scope: InputParameterScope.Client, + isEndpoint: true) + }; + var client = InputFactory.Client("TestClient", parameters: inputParameters); + var clientProvider = new ClientProvider(client); + var settingsProvider = clientProvider.ClientSettings; + + Assert.IsNotNull(settingsProvider); + + var bindCoreMethod = settingsProvider!.Methods.FirstOrDefault(m => m.Signature.Name == "BindCore"); + Assert.IsNotNull(bindCoreMethod); + + var bodyString = bindCoreMethod!.BodyStatements!.ToDisplayString(); + Assert.IsTrue(bodyString.Contains("Uri.TryCreate"), "BindCore should use Uri.TryCreate for Url endpoint binding"); + Assert.IsTrue(bodyString.Contains("UriKind.Absolute"), "BindCore should use UriKind.Absolute for Url endpoint binding"); } [Test] @@ -690,7 +738,7 @@ public void TestSubClient_IndividuallyInitialized_HasEndpointProperty() Assert.IsNotNull(settingsProvider); var endpointProp = settingsProvider!.Properties.FirstOrDefault( - p => p.Name == "Endpoint" && p.Type.Equals(new CSharpType(typeof(Uri), isNullable: true))); + p => p.Name == "Endpoint" && p.Type.Equals(new CSharpType(typeof(string), isNullable: true))); Assert.IsNotNull(endpointProp, "Sub-client settings should have an Endpoint property"); } @@ -758,7 +806,7 @@ public void TestSubClient_IndividuallyInitialized_BindCoreHasEndpointAndOptions( Assert.IsNotNull(bindCoreMethod, "Sub-client settings should have BindCore method"); var bodyString = bindCoreMethod!.BodyStatements!.ToDisplayString(); - Assert.IsTrue(bodyString.Contains("TryCreate"), "BindCore should bind the Endpoint via Uri.TryCreate"); + Assert.IsTrue(bodyString.Contains("IsNullOrEmpty"), "BindCore should bind the Endpoint via string.IsNullOrEmpty for string endpoint"); Assert.IsTrue(bodyString.Contains("GetSection") && bodyString.Contains("Options"), "BindCore should bind the Options section"); } From 82a38d4be5ceddaf410681416af932ea2590ab0b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:50:46 +0000 Subject: [PATCH 3/5] refactor: replace EndpointPropertyName+EndpointPropertyType with EndpointProperty PropertyProvider, add TestData-based tests and constructor tests Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ClientProvider.cs | 6 +- .../src/Providers/ClientSettingsProvider.cs | 33 ++-- .../Providers/ClientSettingsProviderTests.cs | 157 +++++++++--------- ...estGeneratedSettings_WithStringEndpoint.cs | 32 ++++ .../TestGeneratedSettings_WithUrlEndpoint.cs | 32 ++++ 5 files changed, 164 insertions(+), 96 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestGeneratedSettings_WithStringEndpoint.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestGeneratedSettings_WithUrlEndpoint.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index 9117161aafd..66ce99bc1fa 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -706,7 +706,7 @@ void AppendPublicConstructors( private IEnumerable BuildSettingsConstructors() { - if (ClientSettings == null || ClientSettings.EndpointPropertyName == null) + if (ClientSettings == null || ClientSettings.EndpointProperty == null) { yield break; } @@ -731,8 +731,8 @@ private IEnumerable BuildSettingsConstructors() args.Add(Static(typeof(AuthenticationPolicy)).Invoke("Create", settingsParam)); #pragma warning restore SCME0002 - // endpoint argument - we know EndpointPropertyName is not null at this point - args.Add(new MemberExpression(new NullConditionalExpression(settingsParam), ClientSettings.EndpointPropertyName)); + // endpoint argument - we know EndpointProperty is not null at this point + args.Add(new MemberExpression(new NullConditionalExpression(settingsParam), ClientSettings.EndpointProperty.Name)); // other required parameters (non-auth, non-endpoint) in primary constructor order foreach (var param in ClientSettings.OtherRequiredParams) 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 e7d5fe26d60..c8408170637 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 @@ -37,11 +37,20 @@ internal ClientSettingsProvider(InputClient inputClient, ClientProvider clientPr var inputEndpointParam = inputClient.Parameters .FirstOrDefault(p => p is InputEndpointParameter ep && ep.IsEndpoint) as InputEndpointParameter; - EndpointPropertyName = inputEndpointParam?.Name.ToIdentifierName(); if (inputEndpointParam != null) { - EndpointPropertyType = ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(inputEndpointParam.Type); + var endpointType = ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(inputEndpointParam.Type); + if (endpointType != null) + { + EndpointProperty = new PropertyProvider( + null, + MethodSignatureModifiers.Public, + endpointType.WithNullable(true), + inputEndpointParam.Name.ToIdentifierName(), + new AutoPropertyBody(true), + this); + } } // Collect non-endpoint, non-apiVersion required parameters (auth params come separately via InputClient.Auth) @@ -54,10 +63,8 @@ internal ClientSettingsProvider(InputClient inputClient, ClientProvider clientPr .ToList(); } - internal string? EndpointPropertyName { get; } - - /// Gets the CSharp type of the endpoint parameter (e.g. string, Uri). - internal CSharpType? EndpointPropertyType { get; } + /// Gets the endpoint property (name and type) when the client has an endpoint parameter. + internal PropertyProvider? EndpointProperty { get; } /// Gets non-endpoint, non-auth required parameters that have settings properties. internal IReadOnlyList OtherRequiredParams { get; } @@ -82,15 +89,9 @@ protected override PropertyProvider[] BuildProperties() { var properties = new List(); - if (EndpointPropertyName != null && EndpointPropertyType != null) + if (EndpointProperty != null) { - properties.Add(new PropertyProvider( - null, - MethodSignatureModifiers.Public, - EndpointPropertyType.WithNullable(true), - EndpointPropertyName, - new AutoPropertyBody(true), - this)); + properties.Add(EndpointProperty); } foreach (var param in OtherRequiredParams) @@ -124,9 +125,9 @@ protected override MethodProvider[] BuildMethods() var sectionParam = new ParameterProvider("section", $"The configuration section.", IConfigurationSectionType); var body = new List(); - if (EndpointPropertyName != null && EndpointPropertyType != null) + if (EndpointProperty != null) { - AppendBindingForProperty(body, sectionParam, EndpointPropertyName, EndpointPropertyName.ToVariableName(), EndpointPropertyType); + AppendBindingForProperty(body, sectionParam, EndpointProperty.Name, EndpointProperty.Name.ToVariableName(), EndpointProperty.Type); } foreach (var param in OtherRequiredParams) 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 0c7278798df..1c7be74ccd9 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 @@ -6,6 +6,7 @@ using Microsoft.TypeSpec.Generator.ClientModel.Providers; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Tests.Common; using NUnit.Framework; @@ -42,7 +43,7 @@ public void TestBaseType() } [Test] - public void TestProperties_WithEndpoint() + public void TestGeneratedSettings_WithStringEndpoint() { var inputParameters = new[] { @@ -59,13 +60,9 @@ public void TestProperties_WithEndpoint() Assert.IsNotNull(settingsProvider); - var properties = settingsProvider!.Properties; - // String endpoint parameter should produce a string? property, not Uri? - var endpointProp = properties.FirstOrDefault(p => p.Name == "Endpoint" && p.Type.Equals(new CSharpType(typeof(string), isNullable: true))); - Assert.IsNotNull(endpointProp, "Settings should have an Endpoint property of type string?"); - - var optionsProp = properties.FirstOrDefault(p => p.Name == "Options"); - Assert.IsNotNull(optionsProp, "Settings should have an Options property"); + var writer = new TypeProviderWriter(settingsProvider!); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } [Test] @@ -78,73 +75,12 @@ public void TestProperties_NoEndpoint() // Settings provider should exist but without endpoint-related properties Assert.IsNotNull(settingsProvider); - var endpointProp = settingsProvider!.Properties.FirstOrDefault(p => p.Name == "Endpoint"); + var endpointProp = settingsProvider!.Properties.FirstOrDefault(p => p.Name == "Endpoint" && p.Type.Equals(new CSharpType(typeof(Uri), isNullable: true))); Assert.IsNull(endpointProp, "Settings should not have an Endpoint property when no endpoint parameter exists"); } [Test] - public void TestBindCoreMethod_WithEndpoint() - { - var inputParameters = new[] - { - InputFactory.EndpointParameter( - "endpoint", - InputPrimitiveType.String, - defaultValue: InputFactory.Constant.String("https://default.endpoint.io"), - scope: InputParameterScope.Client, - isEndpoint: true) - }; - var client = InputFactory.Client("TestClient", parameters: inputParameters); - var clientProvider = new ClientProvider(client); - var settingsProvider = clientProvider.ClientSettings; - - Assert.IsNotNull(settingsProvider); - - var methods = settingsProvider!.Methods; - var bindCoreMethod = methods.FirstOrDefault(m => m.Signature.Name == "BindCore"); - Assert.IsNotNull(bindCoreMethod, "Settings should have a BindCore method"); - - // Validate it's protected override - Assert.AreEqual( - MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override, - bindCoreMethod!.Signature.Modifiers); - - // Validate it has section parameter - Assert.AreEqual(1, bindCoreMethod.Signature.Parameters.Count); - Assert.AreEqual("section", bindCoreMethod.Signature.Parameters[0].Name); - - // Validate the body uses string binding for string endpoint parameter - var body = bindCoreMethod.BodyStatements; - Assert.IsNotNull(body); - var bodyString = body!.ToDisplayString(); - Assert.IsTrue(bodyString.Contains("IsNullOrEmpty"), "BindCore should use string.IsNullOrEmpty for string endpoint binding"); - } - - [Test] - public void TestProperties_WithUrlEndpoint() - { - var inputParameters = new[] - { - InputFactory.EndpointParameter( - "endpoint", - InputPrimitiveType.Url, - scope: InputParameterScope.Client, - isEndpoint: true) - }; - var client = InputFactory.Client("TestClient", parameters: inputParameters); - var clientProvider = new ClientProvider(client); - var settingsProvider = clientProvider.ClientSettings; - - Assert.IsNotNull(settingsProvider); - - var properties = settingsProvider!.Properties; - // Url endpoint parameter should produce a Uri? property - var endpointProp = properties.FirstOrDefault(p => p.Name == "Endpoint" && p.Type.Equals(new CSharpType(typeof(Uri), isNullable: true))); - Assert.IsNotNull(endpointProp, "Settings should have an Endpoint property of type Uri? for Url endpoint"); - } - - [Test] - public void TestBindCoreMethod_WithUrlEndpoint() + public void TestGeneratedSettings_WithUrlEndpoint() { var inputParameters = new[] { @@ -160,12 +96,9 @@ public void TestBindCoreMethod_WithUrlEndpoint() Assert.IsNotNull(settingsProvider); - var bindCoreMethod = settingsProvider!.Methods.FirstOrDefault(m => m.Signature.Name == "BindCore"); - Assert.IsNotNull(bindCoreMethod); - - var bodyString = bindCoreMethod!.BodyStatements!.ToDisplayString(); - Assert.IsTrue(bodyString.Contains("Uri.TryCreate"), "BindCore should use Uri.TryCreate for Url endpoint binding"); - Assert.IsTrue(bodyString.Contains("UriKind.Absolute"), "BindCore should use UriKind.Absolute for Url endpoint binding"); + var writer = new TypeProviderWriter(settingsProvider!); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } [Test] @@ -838,5 +771,75 @@ public void TestSubClient_IndividuallyInitialized_SettingsBaseType() Assert.AreEqual(ClientSettingsProvider.ClientSettingsType, settingsProvider!.Type.BaseType, "Sub-client settings should inherit from ClientSettings"); } + + [Test] + public void TestSettingsConstructor_WithStringEndpoint() + { + var inputParameters = new[] + { + InputFactory.EndpointParameter( + "endpoint", + InputPrimitiveType.String, + defaultValue: InputFactory.Constant.String("https://default.endpoint.io"), + scope: InputParameterScope.Client, + isEndpoint: true) + }; + var client = InputFactory.Client("TestClient", parameters: inputParameters); + var clientProvider = new ClientProvider(client); + + var settingsConstructor = clientProvider.Constructors.FirstOrDefault(IsSettingsConstructor); + Assert.IsNotNull(settingsConstructor, "Expected a settings constructor for string endpoint"); + + // Validate the initializer references the settings endpoint property + var initializer = settingsConstructor!.Signature.Initializer; + Assert.IsNotNull(initializer); + Assert.IsFalse(initializer!.IsBase, "Settings constructor should use this() initializer"); + + // The initializer should have arguments for auth policy, endpoint, and options + Assert.IsTrue(initializer.Arguments.Count >= 3, + "Settings constructor initializer should have at least 3 arguments (auth, endpoint, options)"); + + // Validate the endpoint argument references settings?.Endpoint + var endpointArg = initializer.Arguments[1].ToDisplayString(); + Assert.IsTrue(endpointArg.Contains("Endpoint"), + $"Endpoint argument should reference Endpoint property, got: {endpointArg}"); + } + + [Test] + public void TestSettingsConstructor_WithUrlEndpoint() + { + var inputParameters = new[] + { + InputFactory.EndpointParameter( + "endpoint", + InputPrimitiveType.Url, + scope: InputParameterScope.Client, + isEndpoint: true) + }; + var client = InputFactory.Client("TestClient", parameters: inputParameters); + var clientProvider = new ClientProvider(client); + + var settingsConstructor = clientProvider.Constructors.FirstOrDefault(IsSettingsConstructor); + Assert.IsNotNull(settingsConstructor, "Expected a settings constructor for URL endpoint"); + + // Validate the initializer references the settings endpoint property + var initializer = settingsConstructor!.Signature.Initializer; + Assert.IsNotNull(initializer); + Assert.IsFalse(initializer!.IsBase, "Settings constructor should use this() initializer"); + + // The initializer should have arguments for auth policy, endpoint, and options + Assert.IsTrue(initializer.Arguments.Count >= 3, + "Settings constructor initializer should have at least 3 arguments (auth, endpoint, options)"); + + // Validate the endpoint argument references settings?.Endpoint + var endpointArg = initializer.Arguments[1].ToDisplayString(); + Assert.IsTrue(endpointArg.Contains("Endpoint"), + $"Endpoint argument should reference Endpoint property, got: {endpointArg}"); + } + + private static bool IsSettingsConstructor(ConstructorProvider c) => + c.Signature?.Initializer != null && + c.Signature?.Modifiers == MethodSignatureModifiers.Public && + c.Signature.Parameters.Any(p => p.Name == "settings"); } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestGeneratedSettings_WithStringEndpoint.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestGeneratedSettings_WithStringEndpoint.cs new file mode 100644 index 00000000000..d788bffbdd1 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestGeneratedSettings_WithStringEndpoint.cs @@ -0,0 +1,32 @@ +// + +#nullable disable + +using System.ClientModel.Primitives; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; + +namespace Sample +{ + [global::System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SCME0002")] + public partial class TestClientSettings : global::System.ClientModel.Primitives.ClientSettings + { + public string Endpoint { get; set; } + + public global::Sample.TestClientOptions Options { get; set; } + + protected override void BindCore(global::Microsoft.Extensions.Configuration.IConfigurationSection section) + { + string endpoint = section["Endpoint"]; + if (!string.IsNullOrEmpty(endpoint)) + { + this.Endpoint = endpoint; + } + global::Microsoft.Extensions.Configuration.IConfigurationSection optionsSection = section.GetSection("Options"); + if (optionsSection.Exists()) + { + this.Options = new global::Sample.TestClientOptions(optionsSection); + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestGeneratedSettings_WithUrlEndpoint.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestGeneratedSettings_WithUrlEndpoint.cs new file mode 100644 index 00000000000..308ed1bed8b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestGeneratedSettings_WithUrlEndpoint.cs @@ -0,0 +1,32 @@ +// + +#nullable disable + +using System; +using System.ClientModel.Primitives; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; + +namespace Sample +{ + [global::System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SCME0002")] + public partial class TestClientSettings : global::System.ClientModel.Primitives.ClientSettings + { + public global::System.Uri Endpoint { get; set; } + + public global::Sample.TestClientOptions Options { get; set; } + + protected override void BindCore(global::Microsoft.Extensions.Configuration.IConfigurationSection section) + { + if (global::System.Uri.TryCreate(section["Endpoint"], global::System.UriKind.Absolute, out global::System.Uri endpoint)) + { + this.Endpoint = endpoint; + } + global::Microsoft.Extensions.Configuration.IConfigurationSection optionsSection = section.GetSection("Options"); + if (optionsSection.Exists()) + { + this.Options = new global::Sample.TestClientOptions(optionsSection); + } + } + } +} From 2ba366236600d6abc8d439617baace4c4d05e072 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:25:47 +0000 Subject: [PATCH 4/5] test: restore assertions alongside testdata, add testdata for constructor + named string endpoint tests Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../Providers/ClientSettingsProviderTests.cs | 75 ++++++++++++++++++- ...neratedSettings_WithNamedStringEndpoint.cs | 32 ++++++++ ...tSettingsConstructor_WithStringEndpoint.cs | 47 ++++++++++++ ...TestSettingsConstructor_WithUrlEndpoint.cs | 51 +++++++++++++ 4 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestGeneratedSettings_WithNamedStringEndpoint.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestSettingsConstructor_WithStringEndpoint.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestSettingsConstructor_WithUrlEndpoint.cs 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 1c7be74ccd9..5647e296e89 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 @@ -60,7 +60,27 @@ public void TestGeneratedSettings_WithStringEndpoint() Assert.IsNotNull(settingsProvider); - var writer = new TypeProviderWriter(settingsProvider!); + // Validate Endpoint property is string? (not Uri?) + var properties = settingsProvider!.Properties; + var endpointProp = properties.FirstOrDefault(p => p.Name == "Endpoint" && p.Type.Equals(new CSharpType(typeof(string), isNullable: true))); + Assert.IsNotNull(endpointProp, "Settings should have an Endpoint property of type string?"); + + var optionsProp = properties.FirstOrDefault(p => p.Name == "Options"); + Assert.IsNotNull(optionsProp, "Settings should have an Options property"); + + // Validate BindCore method + var bindCoreMethod = settingsProvider.Methods.FirstOrDefault(m => m.Signature.Name == "BindCore"); + Assert.IsNotNull(bindCoreMethod, "Settings should have a BindCore method"); + Assert.AreEqual( + MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override, + bindCoreMethod!.Signature.Modifiers); + Assert.AreEqual(1, bindCoreMethod.Signature.Parameters.Count); + Assert.AreEqual("section", bindCoreMethod.Signature.Parameters[0].Name); + var bodyString = bindCoreMethod.BodyStatements!.ToDisplayString(); + Assert.IsTrue(bodyString.Contains("IsNullOrEmpty"), "BindCore should use string.IsNullOrEmpty for string endpoint binding"); + + // Validate full generated output + var writer = new TypeProviderWriter(settingsProvider); var file = writer.Write(); Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } @@ -96,7 +116,19 @@ public void TestGeneratedSettings_WithUrlEndpoint() Assert.IsNotNull(settingsProvider); - var writer = new TypeProviderWriter(settingsProvider!); + // Validate Endpoint property is Uri? + var properties = settingsProvider!.Properties; + var endpointProp = properties.FirstOrDefault(p => p.Name == "Endpoint" && p.Type.Equals(new CSharpType(typeof(Uri), isNullable: true))); + Assert.IsNotNull(endpointProp, "Settings should have an Endpoint property of type Uri?"); + + // Validate BindCore uses Uri.TryCreate + var bindCoreMethod = settingsProvider.Methods.FirstOrDefault(m => m.Signature.Name == "BindCore"); + Assert.IsNotNull(bindCoreMethod); + var bodyString = bindCoreMethod!.BodyStatements!.ToDisplayString(); + Assert.IsTrue(bodyString.Contains("TryCreate"), "BindCore should use Uri.TryCreate for Uri endpoint binding"); + + // Validate full generated output + var writer = new TypeProviderWriter(settingsProvider); var file = writer.Write(); Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } @@ -772,6 +804,35 @@ public void TestSubClient_IndividuallyInitialized_SettingsBaseType() "Sub-client settings should inherit from ClientSettings"); } + [Test] + public void TestGeneratedSettings_WithNamedStringEndpoint() + { + var inputParameters = new[] + { + InputFactory.EndpointParameter( + "fullyQualifiedNamespace", + InputPrimitiveType.String, + scope: InputParameterScope.Client, + isEndpoint: true, + serverUrlTemplate: "https://{fullyQualifiedNamespace}") + }; + var client = InputFactory.Client("TestClient", parameters: inputParameters); + var clientProvider = new ClientProvider(client); + var settingsProvider = clientProvider.ClientSettings; + + Assert.IsNotNull(settingsProvider); + + // Validate FullyQualifiedNamespace property is string? (not Uri?) + var properties = settingsProvider!.Properties; + var endpointProp = properties.FirstOrDefault(p => p.Name == "FullyQualifiedNamespace" && p.Type.Equals(new CSharpType(typeof(string), isNullable: true))); + Assert.IsNotNull(endpointProp, "Settings should have a FullyQualifiedNamespace property of type string?"); + + // Validate full generated output + var writer = new TypeProviderWriter(settingsProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + [Test] public void TestSettingsConstructor_WithStringEndpoint() { @@ -803,6 +864,11 @@ public void TestSettingsConstructor_WithStringEndpoint() var endpointArg = initializer.Arguments[1].ToDisplayString(); Assert.IsTrue(endpointArg.Contains("Endpoint"), $"Endpoint argument should reference Endpoint property, got: {endpointArg}"); + + // Validate full generated client output + var writer = new TypeProviderWriter(clientProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } [Test] @@ -835,6 +901,11 @@ public void TestSettingsConstructor_WithUrlEndpoint() var endpointArg = initializer.Arguments[1].ToDisplayString(); Assert.IsTrue(endpointArg.Contains("Endpoint"), $"Endpoint argument should reference Endpoint property, got: {endpointArg}"); + + // Validate full generated client output + var writer = new TypeProviderWriter(clientProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } private static bool IsSettingsConstructor(ConstructorProvider c) => diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestGeneratedSettings_WithNamedStringEndpoint.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestGeneratedSettings_WithNamedStringEndpoint.cs new file mode 100644 index 00000000000..ec384277371 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestGeneratedSettings_WithNamedStringEndpoint.cs @@ -0,0 +1,32 @@ +// + +#nullable disable + +using System.ClientModel.Primitives; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; + +namespace Sample +{ + [global::System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SCME0002")] + public partial class TestClientSettings : global::System.ClientModel.Primitives.ClientSettings + { + public string FullyQualifiedNamespace { get; set; } + + public global::Sample.TestClientOptions Options { get; set; } + + protected override void BindCore(global::Microsoft.Extensions.Configuration.IConfigurationSection section) + { + string fullyQualifiedNamespace = section["FullyQualifiedNamespace"]; + if (!string.IsNullOrEmpty(fullyQualifiedNamespace)) + { + this.FullyQualifiedNamespace = fullyQualifiedNamespace; + } + global::Microsoft.Extensions.Configuration.IConfigurationSection optionsSection = section.GetSection("Options"); + if (optionsSection.Exists()) + { + this.Options = new global::Sample.TestClientOptions(optionsSection); + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestSettingsConstructor_WithStringEndpoint.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestSettingsConstructor_WithStringEndpoint.cs new file mode 100644 index 00000000000..ac11b7e4c81 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestSettingsConstructor_WithStringEndpoint.cs @@ -0,0 +1,47 @@ +// + +#nullable disable + +using System; +using System.ClientModel.Primitives; +using System.Diagnostics.CodeAnalysis; + +namespace Sample +{ + public partial class TestClient + { + private readonly global::System.Uri _endpoint; + + public TestClient() : this(new string("https://default.endpoint.io"), new global::Sample.TestClientOptions()) + { + } + + internal TestClient(global::System.ClientModel.Primitives.AuthenticationPolicy authenticationPolicy, global::System.Uri endpoint, global::Sample.TestClientOptions options) + { + global::Sample.Argument.AssertNotNull(endpoint, nameof(endpoint)); + + options ??= new global::Sample.TestClientOptions(); + + _endpoint = endpoint; + if ((authenticationPolicy != null)) + { + Pipeline = global::System.ClientModel.Primitives.ClientPipeline.Create(options, Array.Empty(), new global::System.ClientModel.Primitives.PipelinePolicy[] { new global::System.ClientModel.Primitives.UserAgentPolicy(typeof(global::Sample.TestClient).Assembly), authenticationPolicy }, Array.Empty()); + } + else + { + Pipeline = global::System.ClientModel.Primitives.ClientPipeline.Create(options, Array.Empty(), new global::System.ClientModel.Primitives.PipelinePolicy[] { new global::System.ClientModel.Primitives.UserAgentPolicy(typeof(global::Sample.TestClient).Assembly) }, Array.Empty()); + } + } + + public TestClient(global::System.Uri endpoint, global::Sample.TestClientOptions options) : this(null, endpoint, options) + { + } + + [global::System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SCME0002")] + public TestClient(global::Sample.TestClientSettings settings) : this(global::System.ClientModel.Primitives.AuthenticationPolicy.Create(settings), settings?.Endpoint, settings?.Options) + { + } + + public global::System.ClientModel.Primitives.ClientPipeline Pipeline { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestSettingsConstructor_WithUrlEndpoint.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestSettingsConstructor_WithUrlEndpoint.cs new file mode 100644 index 00000000000..ad76eb92090 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestSettingsConstructor_WithUrlEndpoint.cs @@ -0,0 +1,51 @@ +// + +#nullable disable + +using System; +using System.ClientModel.Primitives; +using System.Diagnostics.CodeAnalysis; + +namespace Sample +{ + public partial class TestClient + { + private readonly global::System.Uri _endpoint; + + protected TestClient() + { + } + + public TestClient(global::System.Uri endpoint) : this(endpoint, new global::Sample.TestClientOptions()) + { + } + + internal TestClient(global::System.ClientModel.Primitives.AuthenticationPolicy authenticationPolicy, global::System.Uri endpoint, global::Sample.TestClientOptions options) + { + global::Sample.Argument.AssertNotNull(endpoint, nameof(endpoint)); + + options ??= new global::Sample.TestClientOptions(); + + _endpoint = endpoint; + if ((authenticationPolicy != null)) + { + Pipeline = global::System.ClientModel.Primitives.ClientPipeline.Create(options, Array.Empty(), new global::System.ClientModel.Primitives.PipelinePolicy[] { new global::System.ClientModel.Primitives.UserAgentPolicy(typeof(global::Sample.TestClient).Assembly), authenticationPolicy }, Array.Empty()); + } + else + { + Pipeline = global::System.ClientModel.Primitives.ClientPipeline.Create(options, Array.Empty(), new global::System.ClientModel.Primitives.PipelinePolicy[] { new global::System.ClientModel.Primitives.UserAgentPolicy(typeof(global::Sample.TestClient).Assembly) }, Array.Empty()); + } + } + + public TestClient(global::System.Uri endpoint, global::Sample.TestClientOptions options) : this(null, endpoint, options) + { + } + + [global::System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SCME0002")] + public TestClient(global::Sample.TestClientSettings settings) : this(global::System.ClientModel.Primitives.AuthenticationPolicy.Create(settings), settings?.Endpoint, settings?.Options) + { + } + + public global::System.ClientModel.Primitives.ClientPipeline Pipeline { get; } + } +} From 46a68a44ae6329cdb929aac4a59caaa02ffea0e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:41:46 +0000 Subject: [PATCH 5/5] fix: use serverUrlTemplate in string endpoint constructor test for valid generated code Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../test/Providers/ClientSettingsProviderTests.cs | 4 ++-- .../TestSettingsConstructor_WithStringEndpoint.cs | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) 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 5647e296e89..d864e4b3ea9 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 @@ -841,9 +841,9 @@ public void TestSettingsConstructor_WithStringEndpoint() InputFactory.EndpointParameter( "endpoint", InputPrimitiveType.String, - defaultValue: InputFactory.Constant.String("https://default.endpoint.io"), scope: InputParameterScope.Client, - isEndpoint: true) + isEndpoint: true, + serverUrlTemplate: "https://{endpoint}") }; var client = InputFactory.Client("TestClient", parameters: inputParameters); var clientProvider = new ClientProvider(client); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestSettingsConstructor_WithStringEndpoint.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestSettingsConstructor_WithStringEndpoint.cs index ac11b7e4c81..b34c34bc1fa 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestSettingsConstructor_WithStringEndpoint.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestSettingsConstructor_WithStringEndpoint.cs @@ -12,17 +12,19 @@ public partial class TestClient { private readonly global::System.Uri _endpoint; - public TestClient() : this(new string("https://default.endpoint.io"), new global::Sample.TestClientOptions()) + protected TestClient() { } - internal TestClient(global::System.ClientModel.Primitives.AuthenticationPolicy authenticationPolicy, global::System.Uri endpoint, global::Sample.TestClientOptions options) + public TestClient(string endpoint = default) : this(endpoint, new global::Sample.TestClientOptions()) { - global::Sample.Argument.AssertNotNull(endpoint, nameof(endpoint)); + } + internal TestClient(global::System.ClientModel.Primitives.AuthenticationPolicy authenticationPolicy, string endpoint = default, global::Sample.TestClientOptions options) + { options ??= new global::Sample.TestClientOptions(); - _endpoint = endpoint; + _endpoint = new global::System.Uri($"https://{endpoint}"); if ((authenticationPolicy != null)) { Pipeline = global::System.ClientModel.Primitives.ClientPipeline.Create(options, Array.Empty(), new global::System.ClientModel.Primitives.PipelinePolicy[] { new global::System.ClientModel.Primitives.UserAgentPolicy(typeof(global::Sample.TestClient).Assembly), authenticationPolicy }, Array.Empty()); @@ -33,7 +35,7 @@ internal TestClient(global::System.ClientModel.Primitives.AuthenticationPolicy a } } - public TestClient(global::System.Uri endpoint, global::Sample.TestClientOptions options) : this(null, endpoint, options) + public TestClient(string endpoint = default, global::Sample.TestClientOptions options) : this(null, endpoint, options) { }