Skip to content

Commit ef6beb4

Browse files
fengzhou-msftPan Shao
andauthored
[DPG] Add service driven evolution documentation (Azure#27713)
* add skeleton * add service driven evolution doc * Resolve comments * Resolve comments * Resolve comments * Resolve comments Co-authored-by: Pan Shao <pashao@microsoft.com>
1 parent 21104cc commit ef6beb4

File tree

1 file changed

+220
-0
lines changed

1 file changed

+220
-0
lines changed
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# Service driven evolution
2+
3+
As a service evolves with non-breaking changes in the REST API, it may or may not cause breaking changes in the generated SDK. For instance, adding a new method in an existing path or in a new path won't cause breaking changes in the new SDK. See details [here](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#api-versioning).
4+
5+
However, even no breaking changes exist from the REST API perspective, some changes like adding an optional query parameter could still cause breaking changes in the generated SDK. In order to make the SDK backward compatible, we can add customized code such as overload methods for now. We will make it automatically in the near future. The following sections will show some examples of such scenarios and how to handle them.
6+
7+
## A method gets a new optional parameter
8+
9+
Generated code in a V1 client with a required query parameter and an optional query parameter:
10+
11+
``` C#
12+
public virtual async Task<Response> PutRequiredOptionalAsync(string requiredParam, string optionalParam = null, RequestContext context = null)
13+
{
14+
...
15+
try
16+
{
17+
using HttpMessage message = CreatePutRequiredOptionalRequest(requiredParam, optionalParam, context);
18+
return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false);
19+
}
20+
...
21+
}
22+
23+
internal HttpMessage CreatePutRequiredOptionalRequest(string requiredParam, string optionalParam, RequestContext context)
24+
{
25+
...
26+
if (optionalParam != null)
27+
{
28+
uri.AppendQuery("optionalParam", optionalParam, true);
29+
}
30+
...
31+
return message;
32+
}
33+
```
34+
35+
Generated code in a V2 client with a new optional query parameter:
36+
37+
``` C#
38+
public virtual async Task<Response> PutRequiredOptionalAsync(string requiredParam, string optionalParam = null, string newParameter = null, RequestContext context = null)
39+
{
40+
...
41+
try
42+
{
43+
using HttpMessage message = CreatePutRequiredOptionalRequest(requiredParam, optionalParam, newParameter, context);
44+
return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false);
45+
}
46+
...
47+
}
48+
49+
internal HttpMessage CreatePutRequiredOptionalRequest(string requiredParam, string optionalParam, string newParameter, RequestContext context)
50+
{
51+
...
52+
if (optionalParam != null)
53+
{
54+
uri.AppendQuery("optionalParam", optionalParam, true);
55+
}
56+
if (newParameter != null)
57+
{
58+
uri.AppendQuery("new_parameter", newParameter, true);
59+
}
60+
...
61+
return message;
62+
}
63+
```
64+
65+
Since `context` is always the last parameter, V2 inserts the new optional parameter before `context` parameter and this could break code that works with V1 such as `client.PutRequiredOptionalAsync("requiredParam", "optionalParam", ErrorOptions.NoThrow)` (`ErrorOptions.NoThrow` was passed in as `context` parameter but now it's treated as `newParameter`). We can manually add an overload method in V2 which transforms the optional parameters in V1 to required parameters to make V2 backward compatible with V1:
66+
67+
``` C#
68+
public virtual async Task<Response> PutRequiredOptionalAsync(string requiredParam, string optionalParam, RequestContext context)
69+
{
70+
...
71+
try
72+
{
73+
using HttpMessage message = CreatePutRequiredOptionalRequest(requiredParam, optionalParam, null, context);
74+
return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false);
75+
}
76+
...
77+
}
78+
```
79+
In summary of what this evolution looks like now:
80+
```C#
81+
// Generated in V2
82+
public virtual async Task<Response> PutRequiredOptionalAsync(string requiredParam, string optionalParam = null, string newParameter = null, RequestContext context = null)
83+
{
84+
}
85+
86+
// Generated in V2
87+
internal HttpMessage CreatePutRequiredOptionalRequest(string requiredParam, string optionalParam, string newParameter, RequestContext context)
88+
{
89+
}
90+
91+
// Customization method in V2 to be compatible with V1 calls
92+
public virtual async Task<Response> PutRequiredOptionalAsync(string requiredParam, string optionalParam, RequestContext context)
93+
{
94+
}
95+
```
96+
97+
**Notice:**
98+
Now calling `client.PutRequiredOptionalAsync("requiredParam", "optionalParam", ErrorOptions.NoThrow)` with V2 will invoke the `PutRequiredOptionalAsync` method we added. But the `CreatePutRequiredOptionalRequest` method has one more parameter, so we should call it with `newParameter` `null` as `CreatePutRequiredOptionalRequest(requiredParam, optionalParam, null, context);`.
99+
100+
## A method changes a required parameter to optional
101+
102+
Generated code in a V1 client with two required parameters:
103+
104+
``` C#
105+
public virtual async Task<Response> PutRequiredOptionalAsync(string param1, string param2, RequestContext context = null)
106+
{
107+
...
108+
try
109+
{
110+
using HttpMessage message = CreatePutRequiredOptionalRequest(param1, param2, context);
111+
return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false);
112+
}
113+
...
114+
}
115+
```
116+
117+
**Required input is changed to an optional input when it is in the last position (i.e., `param2`).**
118+
119+
This case is safe to generate as:
120+
121+
``` C#
122+
public virtual async Task<Response> PutRequiredOptionalAsync(string param1, string param2 = null, RequestContext context = null)
123+
{
124+
...
125+
try
126+
{
127+
using HttpMessage message = CreatePutRequiredOptionalRequest(param1, param2, context);
128+
return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false);
129+
}
130+
...
131+
}
132+
```
133+
134+
**Required input is changed to an optional input when it is not in the last position (e.g., `param1`).**
135+
136+
In this case, below generated code is not safe to go. The result of this would be that any existing code calling the v1 method as `PutRequiredOptionalAsync(param1, param2);` would end up passing the wrong values to the method parameters, without any warnings from the compiler.
137+
``` C#
138+
public virtual async Task<Response> PutRequiredOptionalAsync(string param2, string param1 = null, RequestContext context = null)
139+
{
140+
...
141+
try
142+
{
143+
using HttpMessage message = CreatePutRequiredOptionalRequest(param2, param1, context);
144+
return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false);
145+
}
146+
...
147+
}
148+
```
149+
In this case, you might need to
150+
1. Change the V2 method name to another name (e.g., `PutRequiredOptionalV1Async`).
151+
2. Put back the V1 method implementation.
152+
153+
## A new body type is added
154+
155+
Generated code in a V1 client with only `application/json` as the content type:
156+
157+
``` C#
158+
public virtual async Task<Response> PostParametersAsync(RequestContent content, RequestContext context = null)
159+
{
160+
...
161+
try
162+
{
163+
using HttpMessage message = CreatePostParametersRequest(content, context);
164+
return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false);
165+
}
166+
...
167+
}
168+
169+
internal HttpMessage CreatePostParametersRequest(RequestContent content, RequestContext context)
170+
{
171+
...
172+
request.Headers.Add("Content-Type", "application/json");
173+
request.Content = content;
174+
return message;
175+
}
176+
```
177+
178+
Generated code in a V2 client with `image/jpeg` as a new content type in addition to `application/json`:
179+
180+
``` C#
181+
/// <param name="contentType"> Body Parameter content-type. Allowed values: &quot;application/json&quot; | &quot;image/jpeg&quot;. </param>
182+
public virtual async Task<Response> PostParametersAsync(RequestContent content, ContentType contentType, RequestContext context = null)
183+
{
184+
...
185+
try
186+
{
187+
using HttpMessage message = CreatePostParametersRequest(content, contentType, context);
188+
return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false);
189+
}
190+
...
191+
}
192+
193+
internal HttpMessage CreatePostParametersRequest(RequestContent content, ContentType contentType, RequestContext context)
194+
{
195+
...
196+
request.Headers.Add("Content-Type", contentType.ToString());
197+
request.Content = content;
198+
return message;
199+
}
200+
```
201+
202+
Since the V2 client added a required parameter to an existing client method, it breaks the code that is written for the V1 client. We can manually add an overload method that has the same signature as V1 to make it backward compatible:
203+
204+
``` C#
205+
[EditorBrowsable(EditorBrowsableState.Never)]
206+
public virtual async Task<Response> PostParametersAsync(RequestContent content, RequestContext context = null)
207+
{
208+
...
209+
try
210+
{
211+
using HttpMessage message = CreatePostParametersRequest(content, ContentType.ApplicationJson, context);
212+
return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false);
213+
}
214+
...
215+
}
216+
```
217+
218+
**Notice:**
219+
1. We leverage the `CreatePostParametersRequest` method in V2 but always pass in the `ContentType.ApplicationJson` as the `contentType` parameter in the manual method to make the behavior align with V1.
220+
2. Whether to add the attribute `[EditorBrowsable(EditorBrowsableState.Never)]` depends on whether you want to discourage callers from using this. E.g., if service add a new ContentType value to improve performance, you should add this attribute so new users wouldn't discover it, while if service has default preference of old ContentType value, you should not add this attribute to make it easy for users to discover the default.

0 commit comments

Comments
 (0)