Skip to content
Open
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 @@ -17,7 +17,9 @@ internal PipelineMessage CreateGetWidgetMetricsRequest(string day, RequestOption
{
ClientUriBuilder uri = new ClientUriBuilder();
uri.Reset(_endpoint);
uri.AppendPath("/metrics/widgets/daysOfWeek/", false);
uri.AppendPath("/metrics/", false);
uri.AppendPath(_metricsNamespace, true);
uri.AppendPath("/widgets/daysOfWeek/", false);
uri.AppendPath(day, true);
PipelineMessage message = Pipeline.CreateMessage(uri.ToUri(), "GET", PipelineMessageClassifier200);
PipelineRequest request = message.Request;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace SampleTypeSpec
public partial class Metrics
{
private readonly Uri _endpoint;
private readonly string _metricsNamespace;

/// <summary> Initializes a new instance of Metrics for mocking. </summary>
protected Metrics()
Expand All @@ -23,30 +24,38 @@ protected Metrics()
/// <summary> Initializes a new instance of Metrics. </summary>
/// <param name="pipeline"> The HTTP pipeline for sending and receiving REST requests and responses. </param>
/// <param name="endpoint"> Service endpoint. </param>
internal Metrics(ClientPipeline pipeline, Uri endpoint)
/// <param name="metricsNamespace"></param>
internal Metrics(ClientPipeline pipeline, Uri endpoint, string metricsNamespace)
{
_endpoint = endpoint;
Pipeline = pipeline;
_metricsNamespace = metricsNamespace;
}

/// <summary> Initializes a new instance of Metrics. </summary>
/// <param name="endpoint"> Service endpoint. </param>
/// <exception cref="ArgumentNullException"> <paramref name="endpoint"/> is null. </exception>
public Metrics(Uri endpoint) : this(endpoint, new SampleTypeSpecClientOptions())
/// <param name="metricsNamespace"></param>
/// <exception cref="ArgumentNullException"> <paramref name="endpoint"/> or <paramref name="metricsNamespace"/> is null. </exception>
/// <exception cref="ArgumentException"> <paramref name="metricsNamespace"/> is an empty string, and was expected to be non-empty. </exception>
public Metrics(Uri endpoint, string metricsNamespace) : this(endpoint, metricsNamespace, new SampleTypeSpecClientOptions())
{
}

/// <summary> Initializes a new instance of Metrics. </summary>
/// <param name="endpoint"> Service endpoint. </param>
/// <param name="metricsNamespace"></param>
/// <param name="options"> The options for configuring the client. </param>
/// <exception cref="ArgumentNullException"> <paramref name="endpoint"/> is null. </exception>
public Metrics(Uri endpoint, SampleTypeSpecClientOptions options)
/// <exception cref="ArgumentNullException"> <paramref name="endpoint"/> or <paramref name="metricsNamespace"/> is null. </exception>
/// <exception cref="ArgumentException"> <paramref name="metricsNamespace"/> is an empty string, and was expected to be non-empty. </exception>
public Metrics(Uri endpoint, string metricsNamespace, SampleTypeSpecClientOptions options)
{
Argument.AssertNotNull(endpoint, nameof(endpoint));
Argument.AssertNotNullOrEmpty(metricsNamespace, nameof(metricsNamespace));

options ??= new SampleTypeSpecClientOptions();

_endpoint = endpoint;
_metricsNamespace = metricsNamespace;
Pipeline = ClientPipeline.Create(options, Array.Empty<PipelinePolicy>(), new PipelinePolicy[] { new UserAgentPolicy(typeof(Metrics).Assembly) }, Array.Empty<PipelinePolicy>());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public partial class SampleTypeSpecClient
}
};
private readonly string _apiVersion;
private readonly string _metricsNamespace;
private AnimalOperations _cachedAnimalOperations;
private PetOperations _cachedPetOperations;
private DogOperations _cachedDogOperations;
Expand All @@ -45,51 +46,63 @@ protected SampleTypeSpecClient()

/// <summary> Initializes a new instance of SampleTypeSpecClient. </summary>
/// <param name="endpoint"> Service endpoint. </param>
/// <param name="metricsNamespace"></param>
/// <param name="credential"> A credential used to authenticate to the service. </param>
/// <exception cref="ArgumentNullException"> <paramref name="endpoint"/> or <paramref name="credential"/> is null. </exception>
public SampleTypeSpecClient(Uri endpoint, ApiKeyCredential credential) : this(endpoint, credential, new SampleTypeSpecClientOptions())
/// <exception cref="ArgumentNullException"> <paramref name="endpoint"/>, <paramref name="metricsNamespace"/> or <paramref name="credential"/> is null. </exception>
/// <exception cref="ArgumentException"> <paramref name="metricsNamespace"/> is an empty string, and was expected to be non-empty. </exception>
public SampleTypeSpecClient(Uri endpoint, string metricsNamespace, ApiKeyCredential credential) : this(endpoint, metricsNamespace, credential, new SampleTypeSpecClientOptions())
{
}

/// <summary> Initializes a new instance of SampleTypeSpecClient. </summary>
/// <param name="endpoint"> Service endpoint. </param>
/// <param name="metricsNamespace"></param>
/// <param name="tokenProvider"> A credential provider used to authenticate to the service. </param>
/// <exception cref="ArgumentNullException"> <paramref name="endpoint"/> or <paramref name="tokenProvider"/> is null. </exception>
public SampleTypeSpecClient(Uri endpoint, AuthenticationTokenProvider tokenProvider) : this(endpoint, tokenProvider, new SampleTypeSpecClientOptions())
/// <exception cref="ArgumentNullException"> <paramref name="endpoint"/>, <paramref name="metricsNamespace"/> or <paramref name="tokenProvider"/> is null. </exception>
/// <exception cref="ArgumentException"> <paramref name="metricsNamespace"/> is an empty string, and was expected to be non-empty. </exception>
public SampleTypeSpecClient(Uri endpoint, string metricsNamespace, AuthenticationTokenProvider tokenProvider) : this(endpoint, metricsNamespace, tokenProvider, new SampleTypeSpecClientOptions())
{
}

/// <summary> Initializes a new instance of SampleTypeSpecClient. </summary>
/// <param name="endpoint"> Service endpoint. </param>
/// <param name="metricsNamespace"></param>
/// <param name="credential"> A credential used to authenticate to the service. </param>
/// <param name="options"> The options for configuring the client. </param>
/// <exception cref="ArgumentNullException"> <paramref name="endpoint"/> or <paramref name="credential"/> is null. </exception>
public SampleTypeSpecClient(Uri endpoint, ApiKeyCredential credential, SampleTypeSpecClientOptions options)
/// <exception cref="ArgumentNullException"> <paramref name="endpoint"/>, <paramref name="metricsNamespace"/> or <paramref name="credential"/> is null. </exception>
/// <exception cref="ArgumentException"> <paramref name="metricsNamespace"/> is an empty string, and was expected to be non-empty. </exception>
public SampleTypeSpecClient(Uri endpoint, string metricsNamespace, ApiKeyCredential credential, SampleTypeSpecClientOptions options)
{
Argument.AssertNotNull(endpoint, nameof(endpoint));
Argument.AssertNotNullOrEmpty(metricsNamespace, nameof(metricsNamespace));
Argument.AssertNotNull(credential, nameof(credential));

options ??= new SampleTypeSpecClientOptions();

_endpoint = endpoint;
_metricsNamespace = metricsNamespace;
_keyCredential = credential;
Pipeline = ClientPipeline.Create(options, Array.Empty<PipelinePolicy>(), new PipelinePolicy[] { new UserAgentPolicy(typeof(SampleTypeSpecClient).Assembly), ApiKeyAuthenticationPolicy.CreateHeaderApiKeyPolicy(_keyCredential, AuthorizationHeader) }, Array.Empty<PipelinePolicy>());
_apiVersion = options.Version;
}

/// <summary> Initializes a new instance of SampleTypeSpecClient. </summary>
/// <param name="endpoint"> Service endpoint. </param>
/// <param name="metricsNamespace"></param>
/// <param name="tokenProvider"> A credential provider used to authenticate to the service. </param>
/// <param name="options"> The options for configuring the client. </param>
/// <exception cref="ArgumentNullException"> <paramref name="endpoint"/> or <paramref name="tokenProvider"/> is null. </exception>
public SampleTypeSpecClient(Uri endpoint, AuthenticationTokenProvider tokenProvider, SampleTypeSpecClientOptions options)
/// <exception cref="ArgumentNullException"> <paramref name="endpoint"/>, <paramref name="metricsNamespace"/> or <paramref name="tokenProvider"/> is null. </exception>
/// <exception cref="ArgumentException"> <paramref name="metricsNamespace"/> is an empty string, and was expected to be non-empty. </exception>
public SampleTypeSpecClient(Uri endpoint, string metricsNamespace, AuthenticationTokenProvider tokenProvider, SampleTypeSpecClientOptions options)
{
Argument.AssertNotNull(endpoint, nameof(endpoint));
Argument.AssertNotNullOrEmpty(metricsNamespace, nameof(metricsNamespace));
Argument.AssertNotNull(tokenProvider, nameof(tokenProvider));

options ??= new SampleTypeSpecClientOptions();

_endpoint = endpoint;
_metricsNamespace = metricsNamespace;
_tokenProvider = tokenProvider;
Pipeline = ClientPipeline.Create(options, Array.Empty<PipelinePolicy>(), new PipelinePolicy[] { new UserAgentPolicy(typeof(SampleTypeSpecClient).Assembly), new BearerTokenPolicy(_tokenProvider, _flows) }, Array.Empty<PipelinePolicy>());
_apiVersion = options.Version;
Expand Down Expand Up @@ -3317,7 +3330,7 @@ public virtual PlantOperations GetPlantOperationsClient()
/// <summary> Initializes a new instance of Metrics. </summary>
public virtual Metrics GetMetricsClient()
{
return Volatile.Read(ref _cachedMetrics) ?? Interlocked.CompareExchange(ref _cachedMetrics, new Metrics(Pipeline, _endpoint), null) ?? _cachedMetrics;
return Volatile.Read(ref _cachedMetrics) ?? Interlocked.CompareExchange(ref _cachedMetrics, new Metrics(Pipeline, _endpoint, _metricsNamespace), null) ?? _cachedMetrics;
}
}
}
9 changes: 7 additions & 2 deletions docs/samples/client/csharp/SampleService/main.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -833,14 +833,19 @@ interface PlantOperations {
};
}

model MetricsClientParams {
metricsNamespace: string;
}

@clientInitialization({
initializedBy: InitializedBy.individually | InitializedBy.parent,
parameters: MetricsClientParams,
})
interface Metrics {
@doc("Get Widget metrics for given day of week")
@get
@route("/metrics/widgets/daysOfWeek")
getWidgetMetrics(@path day: DaysOfWeekExtensibleEnum): {
@route("/metrics/{metricsNamespace}/widgets/daysOfWeek")
getWidgetMetrics(@path metricsNamespace: string, @path day: DaysOfWeekExtensibleEnum): {
numSold: int32;
averagePrice: float32;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,24 @@ private IReadOnlyList<ParameterProvider> GetSubClientInternalConstructorParamete
return subClientParameters;
}

/// <summary>
/// Determines whether this subclient has non-infrastructure parameters that need to be
/// included as parameters in the parent's accessor method rather than stored on the parent.
/// A subclient has accessor-only parameters if it has non-infrastructure parameters
/// (not API versions, not endpoint) that are not present on the parent's InputClient.Parameters.
/// Uses the raw <see cref="InputClient.Parameters"/> to avoid circular lazy-initialization dependencies.
/// </summary>
internal bool HasAccessorOnlyParameters(InputClient parentInputClient)
{
var parentParamNames = parentInputClient.Parameters
.Select(p => p.Name)
.ToHashSet(StringComparer.OrdinalIgnoreCase);

return _inputClient.Parameters
.Where(p => !p.IsApiVersion && !(p is InputEndpointParameter ep && ep.IsEndpoint))
.Any(p => !parentParamNames.Contains(p.Name));
}

private Lazy<IReadOnlyList<ParameterProvider>> _clientParameters;
internal IReadOnlyList<ParameterProvider> ClientParameters => _clientParameters.Value;
private IReadOnlyList<ParameterProvider> GetClientParameters()
Expand Down Expand Up @@ -397,7 +415,10 @@ protected override FieldProvider[] BuildFields()
// add sub-client caching fields
foreach (var subClient in _subClients.Value)
{
if (subClient._clientCachingField != null)
// Only add caching field when the accessor does not require additional parameters.
// If the subclient has parameters that are not on the parent, each accessor call may
// produce a different client instance, so caching is not appropriate.
if (subClient._clientCachingField != null && !subClient.HasAccessorOnlyParameters(_inputClient))
{
fields.Add(subClient._clientCachingField);
}
Expand Down Expand Up @@ -899,8 +920,20 @@ protected override ScmMethodProvider[] BuildMethods()

var cachedClientFieldVar = new VariableExpression(subClient.Type, subClient._clientCachingField.Declaration, IsRef: true);
List<ValueExpression> subClientConstructorArgs = new(3);

// Populate constructor arguments
List<ParameterProvider> accessorMethodParams = [];

// Determine which subclient parameters should be on the accessor method.
// Subclient-specific parameters (not on the parent) need to be passed via the accessor.
var parentEffectiveParamNames = _allClientParameters
.Select(p => p.Name)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var subClientExtraInputParamNames = subClient._inputClient.Parameters
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we actually need this logic to compare subclient and parent parameters? I think any parameters on the subclient are subclient-specific. Is that not the case?

.Where(p => !p.IsApiVersion && !(p is InputEndpointParameter ep && ep.IsEndpoint))
.Where(p => !parentEffectiveParamNames.Contains(p.Name))
.Select(p => p.Name)
.ToHashSet(StringComparer.OrdinalIgnoreCase);

// Populate constructor arguments, collecting extra params for the accessor method signature
foreach (var param in subClient._subClientInternalConstructorParams.Value)
{
if (parentClientProperties.TryGetValue(param.Name, out var parentProperty))
Expand All @@ -920,30 +953,59 @@ protected override ScmMethodProvider[] BuildMethods()
subClientConstructorArgs.Add(correspondingApiVersionField.Field);
}
}
else if (subClientExtraInputParamNames.Contains(param.Name))
{
// This parameter is subclient-specific and not available on the parent --
// expose it as an accessor method parameter.
accessorMethodParams.Add(param);
subClientConstructorArgs.Add(param);
}
// else: infra param (pipeline, auth, endpoint) not found in parent mock — silently skip
}

// Create the interlocked compare exchange expression for the body
var interlockedCompareExchange = Static(typeof(Interlocked)).Invoke(
nameof(Interlocked.CompareExchange),
[cachedClientFieldVar, New.Instance(subClient.Type, subClientConstructorArgs), Null]);
var factoryMethodName = subClient.Name.EndsWith(ClientSuffix, StringComparison.OrdinalIgnoreCase)
? $"Get{subClient.Name}"
: $"Get{subClient.Name}{ClientSuffix}";

var factoryMethod = new ScmMethodProvider(
new(
factoryMethodName,
$"Initializes a new instance of {subClient.Type.Name}",
MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual,
subClient.Type,
null,
[]),
// return Volatile.Read(ref _cachedClient) ?? Interlocked.CompareExchange(ref _cachedClient, new Client(_pipeline, _keyCredential, _endpoint), null) ?? _cachedClient;
Return(
Static(typeof(Volatile)).Invoke(nameof(Volatile.Read), cachedClientFieldVar)
.NullCoalesce(interlockedCompareExchange.NullCoalesce(subClient._clientCachingField))),
this,
ScmMethodKind.Convenience);
ScmMethodProvider factoryMethod;
if (accessorMethodParams.Count > 0)
{
// When the accessor requires extra parameters, caching is not appropriate
// (different parameter values may produce different client instances).
// Return a new instance directly.
factoryMethod = new ScmMethodProvider(
new(
factoryMethodName,
$"Initializes a new instance of {subClient.Type.Name}",
MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual,
subClient.Type,
null,
[.. accessorMethodParams]),
Return(New.Instance(subClient.Type, subClientConstructorArgs)),
this,
ScmMethodKind.Convenience);
}
else
{
// No extra params - use the existing caching pattern
var interlockedCompareExchange = Static(typeof(Interlocked)).Invoke(
nameof(Interlocked.CompareExchange),
[cachedClientFieldVar, New.Instance(subClient.Type, subClientConstructorArgs), Null]);
factoryMethod = new ScmMethodProvider(
new(
factoryMethodName,
$"Initializes a new instance of {subClient.Type.Name}",
MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual,
subClient.Type,
null,
[]),
// return Volatile.Read(ref _cachedClient) ?? Interlocked.CompareExchange(ref _cachedClient, new Client(_pipeline, _keyCredential, _endpoint), null) ?? _cachedClient;
Return(
Static(typeof(Volatile)).Invoke(nameof(Volatile.Read), cachedClientFieldVar)
.NullCoalesce(interlockedCompareExchange.NullCoalesce(subClient._clientCachingField))),
this,
ScmMethodKind.Convenience);
}
methods.Add(factoryMethod);
}

Expand Down
Loading
Loading