Skip to content

Commit f4f4d32

Browse files
authored
Add support for response classification to RequestOptions (Azure#22923)
1 parent bd74270 commit f4f4d32

File tree

8 files changed

+279
-7
lines changed

8 files changed

+279
-7
lines changed

sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,23 @@ public partial class RequestOptions
55
public RequestOptions() { }
66
public RequestOptions(Azure.ResponseStatusOption statusOption) { }
77
public RequestOptions(System.Action<Azure.Core.HttpMessage> perCall) { }
8+
public RequestOptions(params int[] treatAsSuccess) { }
9+
public RequestOptions(int[] statusCodes, Azure.ResponseClassification classification) { }
810
public System.Threading.CancellationToken CancellationToken { get { throw null; } set { } }
911
public Azure.Core.Pipeline.HttpPipelinePolicy? PerCallPolicy { get { throw null; } set { } }
1012
public Azure.ResponseStatusOption StatusOption { get { throw null; } set { } }
13+
public void AddClassifier(System.Func<Azure.Core.HttpMessage, Azure.ResponseClassification?> classifier) { }
14+
public void AddClassifier(int[] statusCodes, Azure.ResponseClassification classification) { }
15+
public static void Apply(Azure.RequestOptions requestOptions, Azure.Core.HttpMessage message) { }
1116
public static implicit operator Azure.RequestOptions (Azure.ResponseStatusOption option) { throw null; }
1217
}
18+
public enum ResponseClassification
19+
{
20+
Retry = 0,
21+
DontRetry = 1,
22+
Throw = 2,
23+
Success = 3,
24+
}
1325
public enum ResponseStatusOption
1426
{
1527
Default = 0,

sdk/core/Azure.Core.Experimental/src/RequestOptions.cs

Lines changed: 174 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.Threading;
67
using Azure.Core;
78
using Azure.Core.Pipeline;
@@ -13,10 +14,14 @@ namespace Azure
1314
/// </summary>
1415
public class RequestOptions
1516
{
17+
private List<HttpMessageClassifier>? _classifiers;
18+
1619
/// <summary>
1720
/// Initializes a new instance of the <see cref="RequestOptions"/> class.
1821
/// </summary>
19-
public RequestOptions() { }
22+
public RequestOptions()
23+
{
24+
}
2025

2126
/// <summary>
2227
/// Initializes a new instance of the <see cref="RequestOptions"/> class using the given <see cref="RequestOptions"/>.
@@ -30,6 +35,48 @@ public RequestOptions() { }
3035
/// <param name="perCall"></param>
3136
public RequestOptions(Action<HttpMessage> perCall) => PerCallPolicy = new ActionPolicy(perCall);
3237

38+
/// <summary>
39+
/// Initializes a new instance of the <see cref="RequestOptions"/> class.
40+
/// </summary>
41+
/// <param name="treatAsSuccess">The status codes to treat as successful.</param>
42+
public RequestOptions(params int[] treatAsSuccess) : this(treatAsSuccess, ResponseClassification.Success)
43+
{
44+
}
45+
46+
/// <summary>
47+
/// Initializes a new instance of the <see cref="RequestOptions"/> class.
48+
/// Applying provided classification to a set of status codes.
49+
/// </summary>
50+
/// <param name="statusCodes">The status codes to classify.</param>
51+
/// <param name="classification">The classification.</param>
52+
public RequestOptions(int[] statusCodes, ResponseClassification classification)
53+
{
54+
AddClassifier(statusCodes, classification);
55+
}
56+
57+
/// <summary>
58+
/// Adds the classification for provided status codes.
59+
/// </summary>
60+
/// <param name="statusCodes">The status codes to classify.</param>
61+
/// <param name="classification">The classification.</param>
62+
public void AddClassifier(int[] statusCodes, ResponseClassification classification)
63+
{
64+
foreach (var statusCode in statusCodes)
65+
{
66+
AddClassifier(message => message.Response.Status == statusCode ? classification : null);
67+
}
68+
}
69+
70+
/// <summary>
71+
/// Adds a function that allows to specify how response would be processed by the pipeline.
72+
/// </summary>
73+
/// <param name="classifier"></param>
74+
public void AddClassifier(Func<HttpMessage, ResponseClassification?> classifier)
75+
{
76+
_classifiers ??= new();
77+
_classifiers.Add(new FuncHttpMessageClassifier(classifier));
78+
}
79+
3380
/// <summary>
3481
/// Initializes a new instance of the <see cref="RequestOptions"/> class using the given <see cref="ResponseStatusOption"/>.
3582
/// </summary>
@@ -52,6 +99,24 @@ public RequestOptions() { }
5299
/// </summary>
53100
public HttpPipelinePolicy? PerCallPolicy { get; set; }
54101

102+
/// <summary>
103+
/// Applies options from <see cref="RequestOptions"/> instance to a <see cref="HttpMessage"/>.
104+
/// </summary>
105+
/// <param name="requestOptions"></param>
106+
/// <param name="message"></param>
107+
public static void Apply(RequestOptions requestOptions, HttpMessage message)
108+
{
109+
if (requestOptions.PerCallPolicy != null)
110+
{
111+
message.SetProperty("RequestOptionsPerCallPolicyCallback", requestOptions.PerCallPolicy);
112+
}
113+
114+
if (requestOptions._classifiers != null)
115+
{
116+
message.ResponseClassifier = new PerCallResponseClassifier(message.ResponseClassifier, requestOptions._classifiers);
117+
}
118+
}
119+
55120
/// <summary>
56121
/// An <see cref="HttpPipelineSynchronousPolicy"/> which invokes an action when a request is being sent.
57122
/// </summary>
@@ -63,5 +128,112 @@ internal class ActionPolicy : HttpPipelineSynchronousPolicy
63128

64129
public override void OnSendingRequest(HttpMessage message) => Action.Invoke(message);
65130
}
131+
132+
private class PerCallResponseClassifier : ResponseClassifier
133+
{
134+
private readonly ResponseClassifier _inner;
135+
private readonly List<HttpMessageClassifier> _classifiers;
136+
137+
public PerCallResponseClassifier(ResponseClassifier inner, List<HttpMessageClassifier> classifiers)
138+
{
139+
_inner = inner;
140+
_classifiers = classifiers;
141+
}
142+
143+
public override bool IsRetriableResponse(HttpMessage message)
144+
{
145+
if (Applies(message, ResponseClassification.DontRetry)) return false;
146+
if (Applies(message, ResponseClassification.Retry)) return true;
147+
148+
return _inner.IsRetriableResponse(message);
149+
}
150+
151+
public override bool IsRetriableException(Exception exception)
152+
{
153+
return _inner.IsRetriableException(exception);
154+
}
155+
156+
public override bool IsRetriable(HttpMessage message, Exception exception)
157+
{
158+
if (Applies(message, ResponseClassification.DontRetry)) return false;
159+
160+
return _inner.IsRetriable(message, exception);
161+
}
162+
163+
public override bool IsErrorResponse(HttpMessage message)
164+
{
165+
if (Applies(message, ResponseClassification.Throw)) return true;
166+
if (Applies(message, ResponseClassification.Success)) return false;
167+
168+
return _inner.IsErrorResponse(message);
169+
}
170+
171+
private bool Applies(HttpMessage message, ResponseClassification responseClassification)
172+
{
173+
foreach (var classifier in _classifiers)
174+
{
175+
if (classifier.TryClassify(message, null, out var c) &&
176+
c == responseClassification)
177+
{
178+
return true;
179+
}
180+
}
181+
182+
return false;
183+
}
184+
}
185+
186+
private abstract class HttpMessageClassifier
187+
{
188+
public abstract bool TryClassify(HttpMessage message, Exception? exception, out ResponseClassification classification);
189+
}
190+
191+
private class FuncHttpMessageClassifier : HttpMessageClassifier
192+
{
193+
private readonly Func<HttpMessage, ResponseClassification?> _func;
194+
195+
public FuncHttpMessageClassifier(Func<HttpMessage, ResponseClassification?> func)
196+
{
197+
_func = func;
198+
}
199+
200+
public override bool TryClassify(HttpMessage message, Exception? exception, out ResponseClassification classification)
201+
{
202+
if (_func(message) is ResponseClassification c)
203+
{
204+
classification = c;
205+
return true;
206+
}
207+
208+
classification = default;
209+
return false;
210+
}
211+
}
212+
}
213+
214+
/// <summary>
215+
/// Specifies how response would be processed by the pipeline and the client.
216+
/// </summary>
217+
public enum ResponseClassification
218+
{
219+
/// <summary>
220+
/// The response would be retried.
221+
/// </summary>
222+
Retry,
223+
224+
/// <summary>
225+
/// The response would be retried.
226+
/// </summary>
227+
DontRetry,
228+
229+
/// <summary>
230+
/// The client would throw an exception for the response.
231+
/// </summary>
232+
Throw,
233+
234+
/// <summary>
235+
/// The client would tread the response a successful.
236+
/// </summary>
237+
Success,
66238
}
67-
}
239+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Azure.Core.TestFramework;
5+
using NUnit.Framework;
6+
7+
namespace Azure.Core.Tests
8+
{
9+
public class RequestOptionsTest
10+
{
11+
[Test]
12+
public void CanOverrideDefaultClassificationSuccess()
13+
{
14+
var m = CreateTestMessage(404);
15+
16+
var options = new RequestOptions();
17+
options.AddClassifier(new[] { 404 }, ResponseClassification.Success);
18+
19+
RequestOptions.Apply(options, m);
20+
21+
Assert.False(m.ResponseClassifier.IsErrorResponse(m));
22+
}
23+
24+
[Test]
25+
public void CanOverrideDefaultClassificationThrow()
26+
{
27+
var m = CreateTestMessage(200);
28+
29+
var options = new RequestOptions();
30+
options.AddClassifier(new[] { 200 }, ResponseClassification.Throw);
31+
32+
RequestOptions.Apply(options, m);
33+
34+
Assert.True(m.ResponseClassifier.IsErrorResponse(m));
35+
}
36+
37+
[Test]
38+
public void CanOverrideDefaultClassificationRetry()
39+
{
40+
var m = CreateTestMessage(200);
41+
42+
var options = new RequestOptions();
43+
options.AddClassifier(new[] { 200 }, ResponseClassification.Retry);
44+
45+
RequestOptions.Apply(options, m);
46+
47+
Assert.True(m.ResponseClassifier.IsRetriableResponse(m));
48+
}
49+
50+
[Test]
51+
public void CanOverrideDefaultClassificationNoRetry()
52+
{
53+
var m = CreateTestMessage(500);
54+
55+
var options = new RequestOptions();
56+
options.AddClassifier(new[] { 500 }, ResponseClassification.DontRetry);
57+
58+
RequestOptions.Apply(options, m);
59+
60+
Assert.False(m.ResponseClassifier.IsRetriableResponse(m));
61+
}
62+
63+
[Test]
64+
public void CanOverrideDefaultClassificationWithFunc()
65+
{
66+
HttpMessage m = new HttpMessage(new MockRequest(), new ResponseClassifier())
67+
{
68+
Response = new MockResponse(500)
69+
};
70+
71+
var options = new RequestOptions();
72+
options.AddClassifier(_ => ResponseClassification.Success);
73+
74+
RequestOptions.Apply(options, m);
75+
76+
Assert.False(m.ResponseClassifier.IsErrorResponse(m));
77+
}
78+
79+
private static HttpMessage CreateTestMessage(int status)
80+
{
81+
HttpMessage m = new HttpMessage(new MockRequest(), new ResponseClassifier())
82+
{
83+
Response = new MockResponse(status)
84+
};
85+
return m;
86+
}
87+
}
88+
}

sdk/core/Azure.Core/api/Azure.Core.net461.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ public HttpMessage(Azure.Core.Request request, Azure.Core.ResponseClassifier res
312312
public System.TimeSpan? NetworkTimeout { get { throw null; } set { } }
313313
public Azure.Core.Request Request { get { throw null; } }
314314
public Azure.Response Response { get { throw null; } set { } }
315-
public Azure.Core.ResponseClassifier ResponseClassifier { get { throw null; } }
315+
public Azure.Core.ResponseClassifier ResponseClassifier { get { throw null; } set { } }
316316
public void Dispose() { }
317317
public System.IO.Stream? ExtractResponseContent() { throw null; }
318318
public void SetProperty(string name, object value) { }

sdk/core/Azure.Core/api/Azure.Core.net5.0.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ public HttpMessage(Azure.Core.Request request, Azure.Core.ResponseClassifier res
312312
public System.TimeSpan? NetworkTimeout { get { throw null; } set { } }
313313
public Azure.Core.Request Request { get { throw null; } }
314314
public Azure.Response Response { get { throw null; } set { } }
315-
public Azure.Core.ResponseClassifier ResponseClassifier { get { throw null; } }
315+
public Azure.Core.ResponseClassifier ResponseClassifier { get { throw null; } set { } }
316316
public void Dispose() { }
317317
public System.IO.Stream? ExtractResponseContent() { throw null; }
318318
public void SetProperty(string name, object value) { }

sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ public HttpMessage(Azure.Core.Request request, Azure.Core.ResponseClassifier res
312312
public System.TimeSpan? NetworkTimeout { get { throw null; } set { } }
313313
public Azure.Core.Request Request { get { throw null; } }
314314
public Azure.Response Response { get { throw null; } set { } }
315-
public Azure.Core.ResponseClassifier ResponseClassifier { get { throw null; } }
315+
public Azure.Core.ResponseClassifier ResponseClassifier { get { throw null; } set { } }
316316
public void Dispose() { }
317317
public System.IO.Stream? ExtractResponseContent() { throw null; }
318318
public void SetProperty(string name, object value) { }

sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ public HttpMessage(Azure.Core.Request request, Azure.Core.ResponseClassifier res
312312
public System.TimeSpan? NetworkTimeout { get { throw null; } set { } }
313313
public Azure.Core.Request Request { get { throw null; } }
314314
public Azure.Response Response { get { throw null; } set { } }
315-
public Azure.Core.ResponseClassifier ResponseClassifier { get { throw null; } }
315+
public Azure.Core.ResponseClassifier ResponseClassifier { get { throw null; } set { } }
316316
public void Dispose() { }
317317
public System.IO.Stream? ExtractResponseContent() { throw null; }
318318
public void SetProperty(string name, object value) { }

sdk/core/Azure.Core/src/HttpMessage.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public Response Response
6868
/// <summary>
6969
/// The <see cref="ResponseClassifier"/> instance to use for response classification during pipeline invocation.
7070
/// </summary>
71-
public ResponseClassifier ResponseClassifier { get; }
71+
public ResponseClassifier ResponseClassifier { get; set; }
7272

7373
/// <summary>
7474
/// Gets or sets the value indicating if response would be buffered as part of the pipeline. Defaults to true.

0 commit comments

Comments
 (0)