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
82 changes: 82 additions & 0 deletions packages/http-client-csharp/.tspd/docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,88 @@ namespace Azure.Service.Operations

</details>

## Customize a generated client method's signature
Comment thread
JoshLove-msft marked this conversation as resolved.

Declare a `partial` method (without a body) on the generated client class with the same name and parameter types as a generated protocol or convenience method. The generator emits a matching partial implementation, taking the modifiers and parameter names from your declaration while keeping its generated body.

This applies specifically to **client methods** (protocol and convenience methods on the generated `*Client` class). Other generated members (model constructors, model serialization methods, model factories, etc.) should be customized using `[CodeGenSuppress]` + a hand-written replacement, or one of the other techniques in this document.

Use this when you want to keep the generated body but tweak the surface — typically:

- Tighten the access modifier (`public` → `internal`).
- Add `virtual` / `override` / `new` modifiers.
- Rename a parameter to something more idiomatic.

This is preferred over `[CodeGenSuppress]` + a hand-written replacement when the only thing you want to change is the signature, because you stay in lock-step with the generated body — future regenerations automatically pick up changes (new optional parameters, body changes from spec edits, etc.).

### What is supported

- **Modifier changes** (access modifier, `virtual`, `override`, `new`, `unsafe`).
- **Parameter renames.** The generator clones each parameter with the customer-chosen name while preserving all internal metadata (parameter location, wire info, spread source, validation, …) so the generated body keeps compiling.
- Both protocol method overloads and convenience method overloads (sync and async) on the generated client. Each overload must be customized independently.

### What is NOT supported

- **Renaming the method itself.** Matching is by `(method name, ordered parameter type list)`. To rename, use `[CodeGenSuppress]` + a hand-written method that delegates to the underlying request/pipeline machinery.
- **Changing parameter types.** The parameter type list must match the generated signature exactly (matched by type name). Replacing a parameter type with a different type — even an implicitly convertible one — will simply fail to match and the partial decl will be ignored. To project a different type, use `[CodeGenSuppress]` + a hand-written wrapper that converts and forwards.
- **Adding/removing/reordering parameters.** The parameter type list must match the generated signature exactly. To restructure, use `[CodeGenSuppress]`.
- **Default values on the partial implementation.** C# requires partial method implementations to have all parameters required, so any default values on your partial declaration are stripped on the generated impl. Callers still see the defaults from your declaration in custom code.
- **Non-client members** (models, serialization methods, model factories). Use `[CodeGenSuppress]` for these.

<details>

**Generated code before (Generated/TestClient.cs):**

```C#
namespace Azure.Service
{
public partial class TestClient
{
public virtual ClientResult HelloAgain(BinaryContent p1, RequestOptions options = null)
{
Argument.AssertNotNull(p1, nameof(p1));
using PipelineMessage message = CreateRequest(p1, options);
return ClientResult.FromResponse(Pipeline.ProcessMessage(message, options));
}
}
}
```

**Add customized client (TestClient.cs):**

```C#
namespace Azure.Service
{
public partial class TestClient
{
// Renames `p1` to `content` and changes accessibility from public to internal.
internal partial ClientResult HelloAgain(BinaryContent content, RequestOptions options);
}
}
```

**Generated code after (Generated/TestClient.cs):**

```diff
namespace Azure.Service
{
public partial class TestClient
{
- public virtual ClientResult HelloAgain(BinaryContent p1, RequestOptions options = null)
+ internal partial ClientResult HelloAgain(BinaryContent content, RequestOptions options)
{
- Argument.AssertNotNull(p1, nameof(p1));
- using PipelineMessage message = CreateRequest(p1, options);
+ Argument.AssertNotNull(content, nameof(content));
+ using PipelineMessage message = CreateRequest(content, options);
return ClientResult.FromResponse(Pipeline.ProcessMessage(message, options));
}
}
}
```

</details>

## Replace any generated member

Works for model and client properties, methods, constructors etc.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,39 +110,77 @@ protected virtual IReadOnlyList<ScmMethodProvider> BuildMethods()

private ScmMethodProvider BuildConvenienceMethod(MethodProvider protocolMethod, bool isAsync)
{
if (EnclosingType is not ClientProvider)
if (EnclosingType is not ClientProvider client)
{
throw new InvalidOperationException("Protocol methods can only be built for client types.");
}

var methodSignature = new MethodSignature(
isAsync ? ServiceMethod.Name + "Async" : ServiceMethod.Name,
DocHelpers.GetFormattableDescription(ServiceMethod.Operation.Summary, ServiceMethod.Operation.Doc) ?? FormattableStringHelpers.FromString(ServiceMethod.Operation.Name),
protocolMethod.Signature.Modifiers,
GetResponseType(ServiceMethod.Operation.Responses, true, isAsync, out var responseBodyType),
null,
[.. ConvenienceMethodParameters, ScmKnownParameters.CancellationToken]);
var methodName = isAsync ? ServiceMethod.Name + "Async" : ServiceMethod.Name;
ParameterProvider[] signatureParameters = [.. ConvenienceMethodParameters, ScmKnownParameters.CancellationToken];

// Detect a partial method declaration in the client's custom code matching this convenience method.
MethodSignature? customSignature = null;
PartialMethodCustomization.TryFindCustomSignature(client, methodName, signatureParameters, out customSignature);

// Parameters used to construct the method body. When customizing, we clone the generator's
// parameter providers with the customer's names but keep all generator metadata so that the
// body construction (param conversions, request invocation, etc.) still works.
IReadOnlyList<ParameterProvider> convenienceBodyParameters;

MethodSignature methodSignature;

if (customSignature != null)
{
var renamedSignatureParameters = PartialMethodCustomization.RenameAndCloneParameters(
signatureParameters,
customSignature.Parameters,
removeDefaults: true);

// The generator-controlled body params are the leading parameters (everything except the trailing CancellationToken).
var bodyParams = new ParameterProvider[ConvenienceMethodParameters.Count];
for (int i = 0; i < ConvenienceMethodParameters.Count; i++)
{
bodyParams[i] = renamedSignatureParameters[i];
}
convenienceBodyParameters = bodyParams;

methodSignature = PartialMethodCustomization.BuildPartialSignature(customSignature, renamedSignatureParameters);
}
else
{
convenienceBodyParameters = ConvenienceMethodParameters;
methodSignature = new MethodSignature(
methodName,
DocHelpers.GetFormattableDescription(ServiceMethod.Operation.Summary, ServiceMethod.Operation.Doc) ?? FormattableStringHelpers.FromString(ServiceMethod.Operation.Name),
protocolMethod.Signature.Modifiers,
GetResponseType(ServiceMethod.Operation.Responses, true, isAsync, out _),
null,
signatureParameters);
}

// Recompute the response body type so we can branch the body accordingly.
GetResponseType(ServiceMethod.Operation.Responses, true, isAsync, out var responseBodyType);

MethodBodyStatement[] methodBody;
TypeProvider? collection = null;
if (_pagingServiceMethod != null)
{
collection = ScmCodeModelGenerator.Instance.TypeFactory.ClientResponseApi.CreateClientCollectionResultDefinition(Client, _pagingServiceMethod, responseBodyType, isAsync);
methodBody = [.. GetPagingMethodBody(collection, ConvenienceMethodParameters, true)];
methodBody = [.. GetPagingMethodBody(collection, convenienceBodyParameters, true)];
}
else if (responseBodyType is null)
{
methodBody =
[
.. GetStackVariablesForProtocolParamConversion(ConvenienceMethodParameters, out var declarations),
.. GetStackVariablesForProtocolParamConversion(convenienceBodyParameters, out var declarations),
Return(This.Invoke(protocolMethod.Signature, [.. GetProtocolMethodArguments(declarations)], isAsync))
];
}
else
{
methodBody =
[
.. GetStackVariablesForProtocolParamConversion(ConvenienceMethodParameters, out var paramDeclarations),
.. GetStackVariablesForProtocolParamConversion(convenienceBodyParameters, out var paramDeclarations),
Declare("result", This.Invoke(protocolMethod.Signature, [.. GetProtocolMethodArguments(paramDeclarations)], isAsync).ToApi<ClientResponseApi>(), out ClientResponseApi result),
.. GetStackVariablesForReturnValueConversion(result, responseBodyType, isAsync, out var resultDeclarations),
IsConvertibleFromBinaryData(responseBodyType)
Expand Down Expand Up @@ -933,21 +971,50 @@ private ScmMethodProvider BuildProtocolMethod(MethodProvider createRequestMethod
}

ParameterProvider[] parameters = [.. requiredParameters, .. optionalParameters, requestOptionsParameter];
var methodName = isAsync ? ServiceMethod.Name + "Async" : ServiceMethod.Name;

// Detect a partial method declaration in the client's custom code matching this protocol method.
// When found, we use the customized signature (modifiers, name, parameter names) and emit the
// generated body using the customized parameter references.
MethodSignature? customSignature = null;
PartialMethodCustomization.TryFindCustomSignature(client, methodName, parameters, out customSignature);

MethodSignature methodSignature;
ParameterProvider[] bodyParameters;

if (customSignature != null)
{
// Partial methods cannot have optional parameters in the implementation.
var requiredCustomParameters = PartialMethodCustomization.RenameAndCloneParameters(
customSignature.Parameters,
customSignature.Parameters,
removeDefaults: true).ToArray();

methodSignature = PartialMethodCustomization.BuildPartialSignature(customSignature, requiredCustomParameters);

var methodSignature = new MethodSignature(
isAsync ? ServiceMethod.Name + "Async" : ServiceMethod.Name,
DocHelpers.GetFormattableDescription(ServiceMethod.Operation.Summary, ServiceMethod.Operation.Doc) ?? FormattableStringHelpers.FromString(ServiceMethod.Operation.Name),
methodModifiers,
GetResponseType(ServiceMethod.Operation.Responses, false, isAsync, out _),
$"The response returned from the service.",
parameters);
bodyParameters = requiredCustomParameters;
// Re-resolve the request options parameter from the customized parameter list so the
// generated body references the user-named options parameter (typically the last param).
requestOptionsParameter = requiredCustomParameters[requiredCustomParameters.Length - 1];
}
else
{
methodSignature = new MethodSignature(
methodName,
DocHelpers.GetFormattableDescription(ServiceMethod.Operation.Summary, ServiceMethod.Operation.Doc) ?? FormattableStringHelpers.FromString(ServiceMethod.Operation.Name),
methodModifiers,
GetResponseType(ServiceMethod.Operation.Responses, false, isAsync, out _),
$"The response returned from the service.",
parameters);
bodyParameters = parameters;
}

TypeProvider? collection = null;
MethodBodyStatement[] methodBody;
if (_pagingServiceMethod != null)
{
collection = ScmCodeModelGenerator.Instance.TypeFactory.ClientResponseApi.CreateClientCollectionResultDefinition(Client, _pagingServiceMethod, null, isAsync);
methodBody = [.. GetPagingMethodBody(collection, parameters, false)];
methodBody = [.. GetPagingMethodBody(collection, bodyParameters, false)];
}
else
{
Expand All @@ -956,7 +1023,7 @@ private ScmMethodProvider BuildProtocolMethod(MethodProvider createRequestMethod
[
UsingDeclare("message", ScmCodeModelGenerator.Instance.TypeFactory.HttpMessageApi.HttpMessageType,
This.Invoke(createRequestMethod.Signature,
[.. parameters.Select(p => (ValueExpression)p)]), out var message),
[.. bodyParameters.Select(p => (ValueExpression)p)]), out var message),
Return(ScmCodeModelGenerator.Instance.TypeFactory.ClientResponseApi.ToExpression().FromResponse(client
.PipelineProperty.Invoke(processMessageName, [message, requestOptionsParameter], isAsync, true, extensionType: _clientPipelineExtensionsDefinition.Type)))
];
Expand Down
Loading
Loading