Skip to content
Draft
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
8 changes: 8 additions & 0 deletions packages/http-client-csharp/emitter/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface CSharpEmitterOptions {
"sdk-context-options"?: CreateSdkContextOptions;
"generate-protocol-methods"?: boolean;
"generate-convenience-methods"?: boolean;
"generate-method-instrumentation"?: boolean;
"package-name"?: string;
license?: {
name: string;
Expand Down Expand Up @@ -61,6 +62,12 @@ export const CSharpEmitterOptionsSchema: JSONSchemaType<CSharpEmitterOptions> =
description:
"Set to `false` to skip generation of convenience methods. The default value is `true`.",
},
"generate-method-instrumentation": {
type: "boolean",
nullable: true,
description:
"Set to `false` to disable generation of ActivitySource-based distributed tracing instrumentation in client methods. The default value is `true`.",
},
"unreferenced-types-handling": {
type: "string",
enum: ["removeOrInternalize", "internalize", "keepAll"],
Expand Down Expand Up @@ -156,6 +163,7 @@ export const defaultOptions = {
"save-inputs": false,
"generate-protocol-methods": true,
"generate-convenience-methods": true,
"generate-method-instrumentation": true,
"package-name": undefined,
debug: undefined,
logLevel: LoggerLevel.INFO,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
Expand Down Expand Up @@ -59,6 +60,7 @@ private record ApiVersionFields(FieldProvider Field, PropertyProvider? Correspon
private Dictionary<InputOperation, ScmMethodProviderCollection>? _methodCache;
private Dictionary<InputOperation, ScmMethodProviderCollection> MethodCache => _methodCache ??= [];
private TypeProvider? _backCompatProvider;
private FieldProvider? _activitySourceField;

/// <summary>
/// Gets the effective type provider to use for backward compatibility checks.
Expand Down Expand Up @@ -366,6 +368,22 @@ private IReadOnlyList<ParameterProvider> GetClientParameters()
public PropertyProvider PipelineProperty { get; }
public FieldProvider EndpointField { get; }

/// <summary>
/// Gets the ActivitySource field used for distributed tracing instrumentation.
/// Returns <c>null</c> if method instrumentation is disabled.
/// </summary>
internal FieldProvider? ActivitySourceField => ScmCodeModelGenerator.Instance.Configuration.GenerateMethodInstrumentation
? _activitySourceField ??= BuildActivitySourceField()
: null;

private FieldProvider BuildActivitySourceField()
=> new FieldProvider(
FieldModifiers.Private | FieldModifiers.Static | FieldModifiers.ReadOnly,
typeof(ActivitySource),
"_activitySource",
this,
initializationValue: New.Instance(typeof(ActivitySource), Literal(ScmCodeModelGenerator.Instance.Configuration.PackageName)));

public IReadOnlyList<ClientProvider> SubClients => _subClients.Value;

protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", $"{Name}.cs");
Expand All @@ -376,6 +394,12 @@ protected override FieldProvider[] BuildFields()
{
List<FieldProvider> fields = [EndpointField];

// Add ActivitySource field for distributed tracing instrumentation (only for root clients)
if (ActivitySourceField != null)
{
fields.Add(ActivitySourceField);
}

if (_apiKeyAuthFields != null)
{
fields.Add(_apiKeyAuthFields.AuthField);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
Expand All @@ -27,6 +28,9 @@ public class CollectionResultDefinition : TypeProvider
protected bool IsAsync { get; }
protected ClientProvider Client { get; }
protected FieldProvider ClientField { get; }
protected FieldProvider? ActivitySourceField { get; }
private string? _executePageRequestMethodName;
private string ExecutePageRequestMethodName => _executePageRequestMethodName ??= IsAsync ? "ExecutePageRequestAsync" : "ExecutePageRequest";

protected InputOperation Operation { get; }
protected InputPagingServiceMetadata Paging { get; }
Expand Down Expand Up @@ -73,6 +77,16 @@ public CollectionResultDefinition(ClientProvider client, InputPagingServiceMetho
Client.Type,
"_client",
this);

if (Client.ActivitySourceField != null)
{
ActivitySourceField = new FieldProvider(
FieldModifiers.Private | FieldModifiers.ReadOnly,
new CSharpType(typeof(ActivitySource), isNullable: true),
"_activitySource",
this);
}

Operation = serviceMethod.Operation;
Paging = serviceMethod.PagingMetadata;
IsAsync = isAsync;
Expand Down Expand Up @@ -187,7 +201,10 @@ protected override string BuildName()
protected override TypeSignatureModifiers BuildDeclarationModifiers()
=> TypeSignatureModifiers.Internal | TypeSignatureModifiers.Partial | TypeSignatureModifiers.Class;

protected override FieldProvider[] BuildFields() => [ClientField, .. RequestFields];
protected override FieldProvider[] BuildFields()
=> ActivitySourceField != null
? [ClientField, ActivitySourceField, .. RequestFields]
: [ClientField, .. RequestFields];

protected override CSharpType[] BuildImplements() =>
(_modelType: ItemModelType, IsAsync) switch
Expand All @@ -204,18 +221,27 @@ protected override ConstructorProvider[] BuildConstructors()
"client",
$"The {Client.Type.Name} client used to send requests.",
Client.Type);
var parameters = new List<ParameterProvider> { clientParameter };
parameters.AddRange(CreateRequestParameters);
ParameterProvider? activitySourceParameter = null;
if (ActivitySourceField != null)
{
activitySourceParameter = new ParameterProvider(
"activitySource",
$"The activity source for distributed tracing.",
new CSharpType(typeof(ActivitySource), isNullable: true),
defaultValue: Null);
parameters.Add(activitySourceParameter);
}
return
[
new ConstructorProvider(
new ConstructorSignature(
Type,
$"Initializes a new instance of {Name}, which is used to iterate over the pages of a collection.",
MethodSignatureModifiers.Public,
[
clientParameter,
.. CreateRequestParameters
]),
BuildConstructorBody(clientParameter),
parameters),
BuildConstructorBody(clientParameter, activitySourceParameter),
this)
];
}
Expand All @@ -230,7 +256,7 @@ protected ValueExpression BuildGetPropertyExpression(IReadOnlyList<string> segme
return ResponseModel.GetPropertyExpression(responseModel, segments);
}

private MethodBodyStatement[] BuildConstructorBody(ParameterProvider clientParameter)
private MethodBodyStatement[] BuildConstructorBody(ParameterProvider clientParameter, ParameterProvider? activitySourceParameter)
{
var statements = new List<MethodBodyStatement>(CreateRequestParameters.Count + 1);

Expand All @@ -242,6 +268,12 @@ private MethodBodyStatement[] BuildConstructorBody(ParameterProvider clientParam
var field = RequestFields[parameterNumber];
statements.Add(field.Assign(parameter).Terminate());
}

if (ActivitySourceField != null && activitySourceParameter != null)
{
statements.Add(ActivitySourceField.Assign(activitySourceParameter).Terminate());
}

return statements.ToArray();
}

Expand Down Expand Up @@ -284,6 +316,11 @@ protected override MethodProvider[] BuildMethods()
this)
};

if (ActivitySourceField != null)
{
methods.Add(BuildExecutePageRequestMethod());
}

if (ItemModelType != null)
{
methods.Add(new MethodProvider(
Expand All @@ -304,6 +341,45 @@ protected override MethodProvider[] BuildMethods()
return methods.ToArray();
}

private MethodProvider BuildExecutePageRequestMethod()
{
var messageParam = new ParameterProvider("message", $"The pipeline message.", new CSharpType(typeof(PipelineMessage)));
string activityName = $"{Client.Name}.{Operation.Name.ToIdentifierName()}";
CSharpType returnType = IsAsync
? new CSharpType(typeof(Task<>), typeof(ClientResult))
: new CSharpType(typeof(ClientResult));
MethodSignatureModifiers modifiers = IsAsync
? MethodSignatureModifiers.Private | MethodSignatureModifiers.Async
: MethodSignatureModifiers.Private;

// using Activity? activity = _activitySource?.StartActivity("...", ActivityKind.Client);
var activityDecl = UsingDeclare(
"activity",
new CSharpType(typeof(Activity), isNullable: true),
ActivitySourceField!.AsValueExpression.NullConditional().Invoke(
nameof(ActivitySource.StartActivity),
[Literal(activityName), FrameworkEnumValue(ActivityKind.Client)]),
out var activityVar);

// try { return ...; } catch (Exception ex) { activity?.SetStatus(ActivityStatusCode.Error, ex.Message); throw; }
var pipelineResponse = ScmCodeModelGenerator.Instance.TypeFactory.ClientResponseApi.ToExpression().FromResponse(
ClientField.Property("Pipeline").ToApi<ClientPipelineApi>().ProcessMessage(
messageParam.ToApi<HttpMessageApi>(),
RequestOptionsField.AsValueExpression.ToApi<HttpRequestOptionsApi>(),
IsAsync)).ToApi<ClientResponseApi>();
var exDecl = new DeclarationExpression(typeof(Exception), "ex", out var exVar);
var catchBlock = new CatchExpression(exDecl,
activityVar.NullConditional().Invoke(nameof(Activity.SetStatus),
[FrameworkEnumValue(ActivityStatusCode.Error), exVar.Property(nameof(Exception.Message))]).Terminate(),
Throw());

MethodBodyStatement[] body = [activityDecl, new TryCatchFinallyStatement(new TryExpression(Return(pipelineResponse)), catchBlock)];
return new MethodProvider(
new MethodSignature(ExecutePageRequestMethodName, null, modifiers, returnType, null, [messageParam]),
body,
this);
}

private MethodBodyStatement[] BuildGetValuesFromPages()
{
var items = GetPropertyExpression(Paging.ItemPropertySegments, PageParameter.AsVariable());
Expand Down Expand Up @@ -380,6 +456,19 @@ private MethodBodyStatement[] BuildGetContinuationToken()
}
}

private ClientResponseApi InvokePageRequest(ScopedApi<PipelineMessage> message)
{
if (ActivitySourceField != null)
{
return This.Invoke(ExecutePageRequestMethodName, [message], IsAsync).ToApi<ClientResponseApi>();
}
return ScmCodeModelGenerator.Instance.TypeFactory.ClientResponseApi.ToExpression().FromResponse(
ClientField.Property("Pipeline").ToApi<ClientPipelineApi>().ProcessMessage(
message.ToApi<HttpMessageApi>(),
RequestOptionsField.AsValueExpression.ToApi<HttpRequestOptionsApi>(),
IsAsync)).ToApi<ClientResponseApi>();
}

private MethodBodyStatement[] BuildGetRawPagesForNextLink()
{
var nextPageVariable = new VariableExpression(typeof(Uri), "nextPageUri");
Expand All @@ -399,11 +488,7 @@ private MethodBodyStatement[] BuildGetRawPagesForNextLink()
{
Declare(
"result",
ScmCodeModelGenerator.Instance.TypeFactory.ClientResponseApi.ToExpression().FromResponse(
ClientField.Property("Pipeline").ToApi<ClientPipelineApi>().ProcessMessage(
message.ToApi<HttpMessageApi>(),
RequestOptionsField.AsValueExpression.ToApi<HttpRequestOptionsApi>(),
IsAsync)).ToApi<ClientResponseApi>(),
InvokePageRequest(message),
out ClientResponseApi result),

// Yield return result
Expand Down Expand Up @@ -438,11 +523,7 @@ private MethodBodyStatement[] BuildGetRawPagesForContinuationToken()
{
Declare(
"result",
ScmCodeModelGenerator.Instance.TypeFactory.ClientResponseApi.ToExpression().FromResponse(
ClientField.Property("Pipeline").ToApi<ClientPipelineApi>().ProcessMessage(
message.ToApi<HttpMessageApi>(),
RequestOptionsField.AsValueExpression.ToApi<HttpRequestOptionsApi>(),
IsAsync)).ToApi<ClientResponseApi>(),
InvokePageRequest(message),
out ClientResponseApi result),

// Yield return result
Expand All @@ -464,16 +545,12 @@ private MethodBodyStatement[] BuildGetRawPagesForSingle()
"message",
InvokeCreateInitialRequest(),
out ScopedApi<PipelineMessage> m);
var pipelineResponse = ScmCodeModelGenerator.Instance.TypeFactory.ClientResponseApi.ToExpression().FromResponse(
ClientField.Property("Pipeline").ToApi<ClientPipelineApi>().ProcessMessage(
m.ToApi<HttpMessageApi>(),
RequestOptionsField.AsValueExpression.ToApi<HttpRequestOptionsApi>(),
IsAsync)).ToApi<ClientResponseApi>();
var pageRequestResult = InvokePageRequest(m);
return
[
pipelineMessageDeclaration,
// Yield return result
YieldReturn(pipelineResponse),
YieldReturn(pageRequestResult),
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.ClientModel.Primitives;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -128,7 +129,10 @@ private ScmMethodProvider BuildConvenienceMethod(MethodProvider protocolMethod,
if (_pagingServiceMethod != null)
{
collection = ScmCodeModelGenerator.Instance.TypeFactory.ClientResponseApi.CreateClientCollectionResultDefinition(Client, _pagingServiceMethod, responseBodyType, isAsync);
methodBody = [.. GetPagingMethodBody(collection, ConvenienceMethodParameters, true)];
// Pass the ActivitySource to the collection so each page request can start its own activity.
// No activity is started here - tracing is per-page in the collection's ExecutePageRequest helper.
ValueExpression? activitySourceExpr = Client.ActivitySourceField?.AsValueExpression;
methodBody = [.. GetPagingMethodBody(collection, ConvenienceMethodParameters, true, activitySourceExpr)];
}
else if (responseBodyType is null)
{
Expand Down Expand Up @@ -165,6 +169,25 @@ .. GetStackVariablesForReturnValueConversion(result, responseBodyType, isAsync,
];
}

// Prepend activity instrumentation statement if enabled (non-paging methods only;
// paging methods start a new activity per page request in ExecutePageRequest/ExecutePageRequestAsync)
if (Client.ActivitySourceField != null && _pagingServiceMethod == null)
{
var activityStatement = UsingDeclare(
"activity",
new CSharpType(typeof(Activity), isNullable: true),
Client.ActivitySourceField.Invoke(
nameof(ActivitySource.StartActivity),
[Literal($"{Client.Name}.{ServiceMethod.Name}"), FrameworkEnumValue(ActivityKind.Client)]),
out var activityVar);
var exceptionDeclaration = new DeclarationExpression(typeof(Exception), "ex", out var exVar);
var catchBlock = new CatchExpression(exceptionDeclaration,
activityVar.NullConditional().Invoke(nameof(Activity.SetStatus),
[FrameworkEnumValue(ActivityStatusCode.Error), exVar.Property(nameof(Exception.Message))]).Terminate(),
Throw());
methodBody = [activityStatement, new TryCatchFinallyStatement(new TryExpression(methodBody), catchBlock)];
}

var convenienceMethod = new ScmMethodProvider(methodSignature, methodBody, EnclosingType, ScmMethodKind.Convenience, collectionDefinition: collection, serviceMethod: ServiceMethod);

if (convenienceMethod.XmlDocs != null)
Expand Down Expand Up @@ -999,20 +1022,19 @@ private ParameterProvider ProcessOptionalParameters(
private IEnumerable<MethodBodyStatement> GetPagingMethodBody(
TypeProvider collection,
IReadOnlyList<ParameterProvider> parameters,
bool isConvenience)
bool isConvenience,
ValueExpression? activityVar = null)
{
if (isConvenience)
{
return
[
.. GetStackVariablesForProtocolParamConversion(ConvenienceMethodParameters, out var declarations),
Return(New.Instance(
collection.Type,
[
This,
.. GetProtocolMethodArguments(declarations)
]))
];
var stackStatements = GetStackVariablesForProtocolParamConversion(ConvenienceMethodParameters, out var declarations).ToArray();
var constructorArgs = new List<ValueExpression> { This };
constructorArgs.AddRange(GetProtocolMethodArguments(declarations));
if (activityVar != null)
{
constructorArgs.Add(activityVar);
}
return [.. stackStatements, Return(New.Instance(collection.Type, [.. constructorArgs]))];
}

return Return(New.Instance(
Expand Down
Loading
Loading