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
Original file line number Diff line number Diff line change
Expand Up @@ -615,21 +615,61 @@ private ScmMethodProvider BuildProtocolMethod(MethodProvider createRequestMethod
}

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

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);
// Check for partial method customization in client's custom code
var customSignature = FindPartialMethodSignature(client, methodName, parameters);

MethodSignature methodSignature;
bool isPartialMethod = false;

if (customSignature != null)
{
// Use the custom signature but ensure all parameters are required for partial methods
var requiredCustomParameters = customSignature.Parameters
.Select(p => p.DefaultValue != null
? new ParameterProvider(p.Name, p.Description, p.Type, defaultValue: null,
isRef: p.IsRef, isOut: p.IsOut, isIn: p.IsIn, isParams: p.IsParams,
attributes: p.Attributes, property: p.Property)
{
Validation = p.Validation,
Field = p.Field
}
: p)
.ToList();

methodSignature = new MethodSignature(
customSignature.Name,
customSignature.Description,
customSignature.Modifiers | MethodSignatureModifiers.Partial,
customSignature.ReturnType,
customSignature.ReturnDescription,
requiredCustomParameters,
customSignature.Attributes,
customSignature.GenericArguments,
customSignature.GenericParameterConstraints,
customSignature.ExplicitInterface,
customSignature.NonDocumentComment);

isPartialMethod = true;
}
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);
}

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, methodSignature.Parameters.ToArray(), false)];
}
else
{
Expand Down Expand Up @@ -661,9 +701,72 @@ private ScmMethodProvider BuildProtocolMethod(MethodProvider createRequestMethod

protocolMethod.XmlDocs.Update(summary: summary, exceptions: exceptions);
}

if (isPartialMethod)
{
protocolMethod.IsPartialMethod = true;
}

return protocolMethod;
}

private MethodSignature? FindPartialMethodSignature(ClientProvider client, string methodName, IReadOnlyList<ParameterProvider> parameters)
{
// Check client's custom code view for partial method declarations
var customMethods = client.CustomCodeView?.Methods ?? [];
var partialMethods = customMethods.Where(m => m.IsPartialMethod);

// Create a temporary signature for matching
var tempSignature = new MethodSignature(
methodName,
null,
MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual,
GetResponseType(ServiceMethod.Operation.Responses, false, methodName.EndsWith("Async"), out _),
null,
parameters);

foreach (var partialMethod in partialMethods)
{
if (IsMatch(partialMethod.Signature, tempSignature))
{
return partialMethod.Signature;
}
}

return null;
}

private static bool IsMatch(MethodSignatureBase customMethod, MethodSignatureBase method)
{
if (customMethod.Parameters.Count != method.Parameters.Count || customMethod.Name != method.Name)
{
return false;
}

for (int i = 0; i < customMethod.Parameters.Count; i++)
{
if (!IsNameMatch(customMethod.Parameters[i].Type, method.Parameters[i].Type))
{
return false;
}
}

return true;
}

private static bool IsNameMatch(CSharpType typeFromCustomization, CSharpType generatedType)
{
// The namespace may not be available for generated types referenced from customization as they
// are not yet generated so Roslyn will not have the namespace information.
if (string.IsNullOrEmpty(typeFromCustomization.Namespace))
{
return typeFromCustomization.Name == generatedType.Name;
}

return typeFromCustomization.Namespace == generatedType.Namespace &&
typeFromCustomization.Name == generatedType.Name;
}

private ParameterProvider ProcessOptionalParameters(
List<ParameterProvider> optionalParameters,
List<ParameterProvider> requiredParameters,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.ClientModel;
using System.Collections.Generic;
using System.Linq;
Expand Down Expand Up @@ -326,5 +327,40 @@ public async Task CanRemoveCachingField()
var cachingField = fields.SingleOrDefault(f => f.Name == "_cachedDog");
Assert.IsNull(cachingField);
}

// Validates that a method signature can be customized using partial methods
[Test]
public async Task CanCustomizeMethodSignature()
{
var inputOperation = InputFactory.Operation("HelloAgain", parameters:
[
InputFactory.BodyParameter("p1", InputFactory.Array(InputPrimitiveType.String))
]);
var inputServiceMethod = InputFactory.BasicServiceMethod("test", inputOperation);
var inputClient = InputFactory.Client("TestClient", methods: [inputServiceMethod]);
var mockGenerator = await MockHelpers.LoadMockGeneratorAsync(
clients: () => [inputClient],
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

// Find the client provider
var clientProvider = mockGenerator.Object.OutputLibrary.TypeProviders.SingleOrDefault(t => t is ClientProvider);
Assert.IsNotNull(clientProvider);

// The generated methods should include HelloAgain as a partial method (protocol method)
var clientProviderMethods = clientProvider!.Methods;
var partialMethod = clientProviderMethods.FirstOrDefault(m =>
m.Signature.Name == "HelloAgain" &&
m.IsPartialMethod &&
m.Signature.Parameters.Any(p => p.Type.Name == "BinaryContent"));
Assert.IsNotNull(partialMethod, "HelloAgain protocol method should be generated as partial");

// Verify it's marked as partial
Assert.IsTrue(partialMethod!.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Partial), "Method should have Partial modifier");

// Verify the signature
Assert.AreEqual("HelloAgain", partialMethod.Signature.Name);
Assert.AreEqual(2, partialMethod.Signature.Parameters.Count);
Assert.AreEqual("content", partialMethod.Signature.Parameters[0].Name, "Parameter name should match custom declaration");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

using System;
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Sample
{
/// <summary></summary>
public partial class TestClient
{
// Partial method declaration - matches protocol method signature
public partial ClientResult HelloAgain(BinaryContent content, RequestOptions options);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public enum MethodSignatureModifiers
Override = 512,
Operator = 1024,
Explicit = 2048,
Implicit = 4096
Implicit = 4096,
Partial = 8192
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ public class MethodProvider

public IReadOnlyList<SuppressionStatement> Suppressions { get; internal set; }

/// <summary>
/// Indicates whether this method should be generated as a partial method.
/// When true, the custom code provides the signature declaration and the generated code provides the implementation.
/// </summary>
public bool IsPartialMethod { get; set; }

// for mocking
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
protected MethodProvider()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,14 @@ protected override MethodProvider[] BuildMethods()
kindOptions: SymbolDisplayKindOptions.None);

AddAdditionalModifiers(methodSymbol, ref modifiers);

// Check if this is a partial method declaration (no body)
bool isPartialDeclaration = IsPartialMethodDeclaration(methodSymbol);
if (isPartialDeclaration)
{
modifiers |= MethodSignatureModifiers.Partial;
}

var explicitInterface = methodSymbol.ExplicitInterfaceImplementations.FirstOrDefault();
var signature = new MethodSignature(
methodSymbol.ToDisplayString(format),
Expand All @@ -217,11 +225,39 @@ protected override MethodProvider[] BuildMethods()
[.. methodSymbol.Parameters.Select(p => ConvertToParameterProvider(methodSymbol, p))],
ExplicitInterface: explicitInterface?.ContainingType?.GetCSharpType());

methods.Add(new MethodProvider(signature, MethodBodyStatement.Empty, this));
var methodProvider = new MethodProvider(signature, MethodBodyStatement.Empty, this);
if (isPartialDeclaration)
{
methodProvider.IsPartialMethod = true;
}

methods.Add(methodProvider);
}
return [.. methods];
}

private bool IsPartialMethodDeclaration(IMethodSymbol methodSymbol)
{
// Check each syntax reference for the method
foreach (var syntaxReference in methodSymbol.DeclaringSyntaxReferences)
{
var syntaxNode = syntaxReference.GetSyntax();
if (syntaxNode is MethodDeclarationSyntax methodSyntax)
{
// Check if it has the partial modifier and no body
bool hasPartialModifier = methodSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
bool hasNoBody = methodSyntax.Body == null && methodSyntax.ExpressionBody == null;

if (hasPartialModifier && hasNoBody)
{
return true;
}
}
}

return false;
}

protected override bool GetIsEnum() => _namedTypeSymbol.TypeKind == TypeKind.Enum;

protected override CSharpType BuildEnumUnderlyingType() => GetIsEnum() ? new CSharpType(typeof(int)) : throw new InvalidOperationException("This type is not an enum");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,85 @@ private protected virtual FieldProvider[] FilterCustomizedFields(FieldProvider[]
private MethodProvider[] BuildMethodsInternal()
{
var methods = new List<MethodProvider>();
var customMethods = CustomCodeView?.Methods ?? [];

// Build a list of partial method declarations from custom code
var partialMethodDeclarations = customMethods
.Where(m => m.IsPartialMethod)
.ToList();

foreach (var method in BuildMethods())
{
if (ShouldGenerate(method))
// If the method is already marked as partial (e.g., by ScmMethodProviderCollection),
// use it as-is without further processing
if (method.IsPartialMethod)
{
methods.Add(method);
continue;
}

// Check if there's a matching partial method declaration in custom code
var matchingPartialDeclaration = partialMethodDeclarations
Copy link
Contributor

@JoshLove-msft JoshLove-msft Sep 30, 2025

Choose a reason for hiding this comment

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

@copilot I think this is too late to be changing the signature. We will need to update it further upstream in RestClientProvider as it will impact the method body statements. Also, remember that for partial methods the generated method will need to have all required parameters.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right that changing the signature after the method body is built is problematic. I've addressed the optional parameters issue in commit 39a938f by ensuring all parameters are required in partial method implementations.

However, the deeper concern about parameter references in the method body is valid - if custom code renames parameters, the body will still reference old names. The current implementation works for the common case where parameter names aren't changed, but full parameter renaming support would require:

  1. Detecting partial declarations before building method bodies, or
  2. Updating parameter references in the body when swapping signatures

Would you prefer I:

  • a) Document that parameter renaming is not currently supported (signature must match exactly)
  • b) Attempt to implement early detection of partial declarations before method body construction
  • c) Leave as-is for now and track parameter renaming as a future enhancement?

Copy link
Contributor

Choose a reason for hiding this comment

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

We will need to implement early detection in RestClientProvider which is responsible for computing the operation parameters.

Copy link
Contributor

Choose a reason for hiding this comment

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

Though thinking about it more, this would only account for customization of client methods and would not handle things like factory methods.

Copy link
Contributor

Choose a reason for hiding this comment

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

@copilot since the main scenario is customizing client method signatures, let's move the detection logic to RestClientProvider.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved the detection logic to ScmMethodProviderCollection.BuildProtocolMethod() in commit 9a7e509. Now partial method signatures are detected and applied before the method body is built, resolving the parameter reference issue. The method body is constructed with the correct custom signature from the start, avoiding mismatches.

.FirstOrDefault(customMethod => IsMatch(customMethod.Signature, method.Signature));

if (matchingPartialDeclaration != null)
{
// Generate as a partial method implementation with the custom signature
var customSignature = matchingPartialDeclaration.Signature;

// Ensure the partial modifier is set
var modifiers = customSignature.Modifiers | MethodSignatureModifiers.Partial;

// For partial methods, all parameters must be required (no default values)
// Create new parameter list with default values removed
var requiredParameters = customSignature.Parameters
.Select(p => p.DefaultValue != null
? new ParameterProvider(p.Name, p.Description, p.Type, defaultValue: null,
isRef: p.IsRef, isOut: p.IsOut, isIn: p.IsIn, isParams: p.IsParams,
attributes: p.Attributes, property: p.Property)
{
Validation = p.Validation,
Field = p.Field
}
: p)
.ToList();

// Create a new signature with the partial modifier and required parameters
var partialSignature = new MethodSignature(
customSignature.Name,
customSignature.Description,
modifiers,
customSignature.ReturnType,
customSignature.ReturnDescription,
requiredParameters,
customSignature.Attributes,
customSignature.GenericArguments,
customSignature.GenericParameterConstraints,
customSignature.ExplicitInterface,
customSignature.NonDocumentComment);

// Create a new method provider with the custom signature and original implementation
var partialMethod = new MethodProvider(
partialSignature,
method.BodyStatements ?? MethodBodyStatement.Empty,
method.EnclosingType,
method.XmlDocs,
method.Suppressions);

if (method.BodyExpression != null)
{
partialMethod = new MethodProvider(
partialSignature,
method.BodyExpression,
method.EnclosingType,
method.XmlDocs,
method.Suppressions);
}

partialMethod.IsPartialMethod = true;
methods.Add(partialMethod);
}
else if (ShouldGenerate(method))
{
methods.Add(method);
}
Expand Down Expand Up @@ -588,6 +664,12 @@ private bool ShouldGenerate(MethodProvider method)
var customMethods = method.EnclosingType.CustomCodeView?.Methods ?? [];
foreach (var customMethod in customMethods)
{
// Skip partial method declarations - they will be handled separately in BuildMethodsInternal
if (customMethod.IsPartialMethod)
{
continue;
}

if (IsMatch(customMethod.Signature, method.Signature))
{
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,8 @@ public IDisposable WriteMethodDeclarationNoScope(MethodSignatureBase methodBase,

if (methodBase is MethodSignature method)
{
AppendRawIf("virtual ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Virtual))
AppendRawIf("partial ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Partial))
.AppendRawIf("virtual ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Virtual))
.AppendRawIf("override ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Override))
.AppendRawIf("new ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.New))
.AppendRawIf("async ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Async));
Expand Down
Loading