Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add document for ambiguity handling #37795

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
225 changes: 225 additions & 0 deletions doc/DataPlaneCodeGeneration/Handle-Ambiguity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# Handle Ambiguity Between Protocol Method and Convenience Method

## Background
Copy link
Member

Choose a reason for hiding this comment

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

instead of Background, i think this is more of a Problem corresponding to the Solution we have below.


In a DPG library, protocol methods will be generated for a swagger input, and both protocol methods and convenience methods will be generated for a TypeSpec input. We expect to make the convenience method an overload of its protocol method, so that users could be very clear they are representing the same operation. For example,

```C#
public virtual async Task<Response<Model>> OperationAsync(string requiredId, string optionalId = null, CancellationToken cancellationToken = default) // Convenience method
{
...
}

public virtual async Task<Response> OperationAsync(string requiredId, string optionalId = null, RequestContext context = null) // Protocol method
{
...
}
```
However, this might lead to the compilation error
```
Error CS0121 The call is ambiguous between the following methods or properties: 'SomeClass.OperationAsync(string, CancellationToken)' and 'SomeClass.OperationAsync(string, RequestContext)'
```
when calling `OperationAsync("<some string>")`.

This article is to explain how we handle this ambiguity issue and how our solution impacts the generated API.

## Solutions

There are two ways to handle this error:

1. Make all the parameters in **protocol methods** required.
```C#
public virtual async Task<Response<Model>> OperationAsync(string requiredId, string optionalId = null, CancellationToken cancellationToken = default) // Convenience method
{
...
}

public virtual async Task<Response> OperationAsync(string requiredId, string optionalId, RequestContext context) // Protocol method
{
...
}
```
2. Append `Value` to the name of the **convenience method**.
```C#
public virtual async Task<Response<Model>> OperationValueAsync(string requiredId, string optionalId = null, CancellationToken cancellationToken = default) // Convenience method
{
...
}

public virtual async Task<Response> OperationAsync(string requiredId, string optionalId = null, RequestContext context = null) // Protocol method
{
...
}
```

Our strategy is:
- If the protocol method hasn’t been GAed, the first solution is taken if there exists an ambiguous call.
- If the protocol method is already GAed, the second solution is taken because we cannot change protocol method anymore.

## Scope of ambiguity we handle
Copy link
Member

Choose a reason for hiding this comment

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

this standalone section of "edge cases" is making the following content a bit off-topic.
Could we make it a note of the section of Solution?


Not all the pairs of protocol method and convenience method will have such problem. Below is a case when there is no ambiguity if users are calling normally by passing a `model` or a `content`. (They could not pass in `null` because it will throw error)
Copy link
Member

Choose a reason for hiding this comment

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

I think here we should summarize this section in case people would fixate on edge cases and ignoring the problem we are trying to solve such as:
It is impossible that the ambiguities could always be fixed when the two methods are overloads of each other, because of the existence of default. We could always use the default keyword which is acceptable by any parameter to construct a scenario that the compiler would complain about ambiguities of invocation. Therefore our goal is not solving all cases of the ambiguities, for the following cases, we will allow them to have ambiguities: ...

Copy link
Member

Choose a reason for hiding this comment

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

And I see we have two principals in this section, we should emphasize them first and then have examples.

```C#
public virtual async Task<Response<Model>> OperationAsync(Model model, CancellationToken cancellationToken = default) // Convenience method
{
Argument.AssertNotNull(model, nameof(model));
...
}

public virtual async Task<Response> OperationAsync(RequestContent content, RequestContext context = null) // Protocol method
{
Argument.AssertNotNull(content, nameof(content));
...
}
```
Not all the ambiguities could be resolved by the first solution. For example, even we handle the example in **Background** section as follows
```C#
public virtual async Task<Response<Model>> OperationAsync(string requiredId, string optionalId = null, CancellationToken cancellationToken = default) // Convenience method
{
...
}

public virtual async Task<Response> OperationAsync(string requiredId, string optionalId, RequestContext context) // Protocol method
{
...
}
```
`OperationAsync("required", "optional", default)` and `OperationAsync("required", "optional", new())` are two examples that still lead to compilation error of ambiguity.

Considering all of these, we only guarantee that we will not generate code which leads to ambiguous call when:
1. The call doesn't have any actual parameter of `null`, `default`, `new()`. For example, we don't care whether `OperationAsync("required", "optional", default)` will lead to ambiguity. Details [here](#special-values-not-taken-into-ambiguity-consideration).
2. The call only has its required parameters passed in. For example, we can only ensure `OperationAsync("required")` is not an ambiguous call.

See below section for more specific examples and corresponding explanation.

## Examples

### Operation without input
Copy link
Member

Choose a reason for hiding this comment

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

the word input is quite ambiguous. requiredId is also an input in the following example.
Should it be Operations without request body?

Spec:
```
model Model {}
op operation(@path requiredId: string, @path optionalId?: string): Model;
```
What we try to generate:
```C#
public virtual async Task<Response<Model>> OperationAsync(string requiredId, string optionalId = null, CancellationToken cancellationToken = default) // Convenience method
{
...
}

public virtual async Task<Response> OperationAsync(string requiredId, string optionalId = null, RequestContext context = null) // Protocol method
{
...
}
```
It only has one required parameter `requiredId`, so the call that we guarantee will not have ambiguity is
```C#
OperationAsync("requiredId");
```
Then we find that this call has ambiguity error, so we have to change our signature by choosing from [one of these solutions](#solution).

### Operation with input
Spec:
```
model Model {}
op operation(@body model: Model): Model;
```
```C#
public virtual async Task<Response<Model>> OperationAsync(Model model, CancellationToken cancellationToken = default) // Convenience method
{
...
}

public virtual async Task<Response> OperationAsync(RequestContent content, RequestContext context = null) // Protocol method
{
...
}
```
No matter if the input model is required or optional in spec, it is a required parameter in the signature. So, what we care about are:
```C#
OperationAsync(new Model());
OperationAsync(new RequestContent());
```
Then we find out these calls are fine, so just keep these signatures.

### Operation without both input and output
Spec:
```
op operation(@path requiredId: string, @path optionalId?: string): void;
```
What we try to generate:
```C#
public virtual async Task<Response> OperationAsync(string requiredId, string optionalId = null, RequestContext context = null) // Protocol method
{
...
}
```
We don't generate convenience method for this case (see [this](#convenience-method-not-always-generated) to see why), so there will be no ambiguity.
### Operation with spread scenario
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
### Operation with spread scenario
### Operation with spread parameters

Spec:
```
alias OperationAlias = {
requiredId: string;
optionalId?: string;
};
op operation(...OperationAlias): void;
```
What we try to generate:
```C#
public virtual async Task<Response> OperationAsync(string requiredId, string optionalId = null, CancellationToken cancellationToken = default) // Convenience method
{
...
}
public virtual async Task<Response> OperationAsync(RequestContent content, RequestContext context = null) // Protocol method
{
...
}
```
The convenience method has a required parameter `requiredId` and protocol method has a required method `content`, so what we guarantee will not have ambiguity are
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
The convenience method has a required parameter `requiredId` and protocol method has a required method `content`, so what we guarantee will not have ambiguity are
The convenience method has a required parameter `requiredId` and protocol method has a required parameter `content`, therefore we guarantee the following invocations will not have ambiguities:

```C#
OperationAsync("requiredId");
OperationAsync(new RequestContent());
```
Then we find these calls are fine, so just keep these signatures.

## Appendix
### Convenience method not always generated
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
### Convenience method not always generated
### Convenience methods are not always generated

There is one [situation](#operation-without-both-input-and-output) we don't generate convenience method. Think about what would be like if we generate the convenience method.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
There is one [situation](#operation-without-both-input-and-output) we don't generate convenience method. Think about what would be like if we generate the convenience method.
There is one [situation](#operation-without-both-input-and-output) we don't generate convenience method, because if we did, the convenience method would have almost exact the same parameter list as the protocol method:

```C#
public virtual async Task<Response> OperationAsync(string requiredId, string optionalId = null, CancellationToken cancellationToken = default) // Convenience method
{
...
RequestContext context = FromCancellationToken(cancellationToken);
OperationAsync(requiredId, optionalId, context);
...
}
public virtual async Task<Response> OperationAsync(string requiredId, string optionalId = null, RequestContext context = null) // Protocol method
{
...
}
```
The convenience method is just a wrapper and doesn't have any value. So, we just skip generating it.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
The convenience method is just a wrapper and doesn't have any value. So, we just skip generating it.
The convenience method is just a wrapper therefore it is not generated.


### Special values not taken into ambiguity consideration
Think about these totally valid overloads that you've seen many times in your daily development.
```C#
class IntModel
{
public int Value {get; set;}
}

class DoubleModel
{
public double Value {get; set;}
}

int Add(IntModel a, IntModel b)
{
return a.Value + b.Value;
}
double Add(DoubleModel a, DoubleModel b)
{
return a.Value + b.Value;
}
```
When calling `Add(null, null)` or `Add(default, default)` or `Add(new(), new())`, ambiguity error still exists. Therefore we don't care about `null`, or `default`, or `new()`.
Comment on lines +203 to +225
Copy link
Member

Choose a reason for hiding this comment

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

I do not think this is necessary to explain, or at least it should not be here as an appendix - it is against the normal reading sequence on human. No one would read a document like this.
In the other comment, I have suggested that we put the reason in the above sections as a note.