Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,30 @@ 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.
// Skip credential types, endpoint types (Uri), and options types as they are handled separately.
var customConstructors = client.CustomCodeView?.Constructors;
if (customConstructors != null)
{
var knownProps = new HashSet<string>(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) &&
!ClientSettingsProvider.IsStandardParameterType(param.Type))
{
properties[propName] = GetJsonSchemaForType(param.Type, localDefinitions);
knownProps.Add(propName);
}
}
}
}

// Add credential reference (defined in System.ClientModel base schema)
properties["Credential"] = new JsonObject
{
Expand Down Expand Up @@ -158,6 +182,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<string>(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}" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,35 @@ 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.
// 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<string>(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) && !IsStandardParameterType(param.Type))
{
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)
{
Expand Down Expand Up @@ -136,6 +165,36 @@ protected override MethodProvider[] BuildMethods()
AppendBindingForProperty(body, sectionParam, propName, param.Name.ToVariableName(), param.Type);
}

// Bind custom constructor parameters from custom code
// Skip credential types, endpoint types (Uri), and options types as they are handled separately.
var customConstructors = _clientProvider.CustomCodeView?.Constructors;
if (customConstructors != null)
Comment thread
JoshLove-msft marked this conversation as resolved.
{
var knownProps = new HashSet<string>();
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) && !IsStandardParameterType(param.Type))
{
AppendBindingForProperty(body, sectionParam, propName, param.Name.ToVariableName(), param.Type);
knownProps.Add(propName);
}
}
}
}

var clientOptions = _clientProvider.EffectiveClientOptions;
if (clientOptions != null)
{
Expand Down Expand Up @@ -391,5 +450,41 @@ internal static void AppendComplexObjectBinding(
ifExistsStatement.Add(This.Property(propName).Assign(New.Instance(type, sectionVar)).Terminate());
body.Add(ifExistsStatement);
}

/// <summary>
/// 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).
/// </summary>
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string>();
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<string>());
}

[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<string>());
}

/// <summary>
/// Test output library that wraps provided TypeProviders.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
}
}
}
Loading
Loading