Skip to content

Commit d0b4b18

Browse files
Added Option to add Interceptors on Client Level (#2118)
* feature: Added Implementation for Interceptors
1 parent 36ebe2f commit d0b4b18

File tree

11 files changed

+292
-22
lines changed

11 files changed

+292
-22
lines changed

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
packages
1+
packages/
2+
nuget.config
3+
24

35
#ignore thumbnails created by windows
46
Thumbs.db

RestSharp.sln

+30
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,36 @@ Global
446446
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|x64.Build.0 = Release|Any CPU
447447
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|x86.ActiveCfg = Release|Any CPU
448448
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|x86.Build.0 = Release|Any CPU
449+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|Any CPU.ActiveCfg = Debug|Any CPU
450+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|Any CPU.Build.0 = Debug|Any CPU
451+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|ARM.ActiveCfg = Debug|Any CPU
452+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|ARM.Build.0 = Debug|Any CPU
453+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|Mixed Platforms.ActiveCfg = Debug|Any CPU
454+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|Mixed Platforms.Build.0 = Debug|Any CPU
455+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|x64.ActiveCfg = Debug|Any CPU
456+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|x64.Build.0 = Debug|Any CPU
457+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|x86.ActiveCfg = Debug|Any CPU
458+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|x86.Build.0 = Debug|Any CPU
459+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
460+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|Any CPU.Build.0 = Debug|Any CPU
461+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|ARM.ActiveCfg = Debug|Any CPU
462+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|ARM.Build.0 = Debug|Any CPU
463+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
464+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
465+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|x64.ActiveCfg = Debug|Any CPU
466+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|x64.Build.0 = Debug|Any CPU
467+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|x86.ActiveCfg = Debug|Any CPU
468+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|x86.Build.0 = Debug|Any CPU
469+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|Any CPU.ActiveCfg = Release|Any CPU
470+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|Any CPU.Build.0 = Release|Any CPU
471+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|ARM.ActiveCfg = Release|Any CPU
472+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|ARM.Build.0 = Release|Any CPU
473+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
474+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|Mixed Platforms.Build.0 = Release|Any CPU
475+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|x64.ActiveCfg = Release|Any CPU
476+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|x64.Build.0 = Release|Any CPU
477+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|x86.ActiveCfg = Release|Any CPU
478+
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|x86.Build.0 = Release|Any CPU
449479
EndGlobalSection
450480
GlobalSection(SolutionProperties) = preSolution
451481
HideSolutionNode = FALSE

gen/SourceGenerator/SourceGenerator.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>netstandard2.0</TargetFramework>

global.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"sdk": {
3+
"version": "7.0.0",
4+
"rollForward": "latestMajor",
5+
"allowPrerelease": false
6+
}
7+
}
+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) .NET Foundation and Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
16+
namespace RestSharp.Interceptors;
17+
18+
/// <summary>
19+
/// Base Interceptor
20+
/// </summary>
21+
public abstract class Interceptor {
22+
/// <summary>
23+
/// Intercepts the request before serialization
24+
/// </summary>
25+
/// <param name="request">RestRequest before serialization</param>
26+
/// <returns>Value Tags</returns>
27+
public virtual ValueTask InterceptBeforeSerialization(RestRequest request) {
28+
return new();
29+
}
30+
31+
/// <summary>
32+
/// Intercepts the request before being sent
33+
/// </summary>
34+
/// <param name="req">HttpRequestMessage before being sent</param>
35+
/// <returns>Value Tags</returns>
36+
public virtual ValueTask InterceptBeforeRequest(HttpRequestMessage req) {
37+
return new();
38+
}
39+
40+
/// <summary>
41+
/// Intercepts the request before being sent
42+
/// </summary>
43+
/// <param name="responseMessage">HttpResponseMessage as received from Server</param>
44+
/// <returns>Value Tags</returns>
45+
public virtual ValueTask InterceptAfterRequest(HttpResponseMessage responseMessage) {
46+
return new();
47+
}
48+
49+
/// <summary>
50+
/// Intercepts the request before deserialization
51+
/// </summary>
52+
/// <param name="response">HttpResponseMessage as received from Server</param>
53+
/// <returns>Value Tags</returns>
54+
public virtual ValueTask InterceptBeforeDeserialize(RestResponse response) {
55+
return new();
56+
}
57+
}

src/RestSharp/Options/RestClientOptions.cs

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
using System.Text;
2323
using RestSharp.Authenticators;
2424
using RestSharp.Extensions;
25+
using RestSharp.Interceptors;
2526

2627
// ReSharper disable UnusedAutoPropertyAccessor.Global
2728
// ReSharper disable PropertyCanBeMadeInitOnly.Global
@@ -64,6 +65,8 @@ public RestClientOptions(string baseUrl) : this(new Uri(Ensure.NotEmptyString(ba
6465
/// </summary>
6566
public IAuthenticator? Authenticator { get; set; }
6667

68+
public List<Interceptor> Interceptors { get; set; } = new();
69+
6770
/// <summary>
6871
/// Passed to <see cref="HttpMessageHandler"/> <code>Credentials</code> property
6972
/// </summary>

src/RestSharp/RestClient.Async.cs

+50-20
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
7777
throw new ObjectDisposedException(nameof(RestClient));
7878
}
7979

80+
await OnBeforeSerialization(request).ConfigureAwait(false);
8081
request.ValidateParameters();
8182
var authenticator = request.Authenticator ?? Options.Authenticator;
8283

@@ -98,38 +99,67 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
9899

99100
var ct = cts.Token;
100101

102+
103+
HttpResponseMessage? responseMessage;
104+
// Make sure we have a cookie container if not provided in the request
105+
CookieContainer cookieContainer = request.CookieContainer ??= new CookieContainer();
106+
107+
var headers = new RequestHeaders()
108+
.AddHeaders(request.Parameters)
109+
.AddHeaders(DefaultParameters)
110+
.AddAcceptHeader(AcceptedContentTypes)
111+
.AddCookieHeaders(url, cookieContainer)
112+
.AddCookieHeaders(url, Options.CookieContainer);
113+
114+
message.AddHeaders(headers);
115+
if (request.OnBeforeRequest != null) await request.OnBeforeRequest(message).ConfigureAwait(false);
116+
await OnBeforeRequest(message).ConfigureAwait(false);
117+
101118
try {
102-
// Make sure we have a cookie container if not provided in the request
103-
var cookieContainer = request.CookieContainer ??= new CookieContainer();
104-
105-
var headers = new RequestHeaders()
106-
.AddHeaders(request.Parameters)
107-
.AddHeaders(DefaultParameters)
108-
.AddAcceptHeader(AcceptedContentTypes)
109-
.AddCookieHeaders(url, cookieContainer)
110-
.AddCookieHeaders(url, Options.CookieContainer);
111-
112-
message.AddHeaders(headers);
113-
114-
if (request.OnBeforeRequest != null) await request.OnBeforeRequest(message).ConfigureAwait(false);
115-
116-
var responseMessage = await HttpClient.SendAsync(message, request.CompletionOption, ct).ConfigureAwait(false);
117-
119+
responseMessage = await HttpClient.SendAsync(message, request.CompletionOption, ct).ConfigureAwait(false);
118120
// Parse all the cookies from the response and update the cookie jar with cookies
119121
if (responseMessage.Headers.TryGetValues(KnownHeaders.SetCookie, out var cookiesHeader)) {
120122
// ReSharper disable once PossibleMultipleEnumeration
121123
cookieContainer.AddCookies(url, cookiesHeader);
122124
// ReSharper disable once PossibleMultipleEnumeration
123125
Options.CookieContainer?.AddCookies(url, cookiesHeader);
124126
}
125-
126-
if (request.OnAfterRequest != null) await request.OnAfterRequest(responseMessage).ConfigureAwait(false);
127-
128-
return new HttpResponse(responseMessage, url, cookieContainer, null, timeoutCts.Token);
129127
}
130128
catch (Exception ex) {
131129
return new HttpResponse(null, url, null, ex, timeoutCts.Token);
132130
}
131+
if (request.OnAfterRequest != null) await request.OnAfterRequest(responseMessage).ConfigureAwait(false);
132+
await OnAfterRequest(responseMessage).ConfigureAwait(false);
133+
return new HttpResponse(responseMessage, url, cookieContainer, null, timeoutCts.Token);
134+
135+
}
136+
137+
/// <summary>
138+
/// Will be called before the Request becomes Serialized
139+
/// </summary>
140+
/// <param name="request">RestRequest before it will be serialized</param>
141+
async Task OnBeforeSerialization(RestRequest request) {
142+
foreach (var interceptor in Options.Interceptors) {
143+
await interceptor.InterceptBeforeSerialization(request); //.ThrowExceptionIfAvailable();
144+
}
145+
}
146+
/// <summary>
147+
/// Will be called before the Request will be sent
148+
/// </summary>
149+
/// <param name="requestMessage">HttpRequestMessage ready to be sent</param>
150+
async Task OnBeforeRequest(HttpRequestMessage requestMessage) {
151+
foreach (var interceptor in Options.Interceptors) {
152+
await interceptor.InterceptBeforeRequest(requestMessage);
153+
}
154+
}
155+
/// <summary>
156+
/// Will be called after the Response has been received from Server
157+
/// </summary>
158+
/// <param name="responseMessage">HttpResponseMessage as received from server</param>
159+
async Task OnAfterRequest(HttpResponseMessage responseMessage) {
160+
foreach (var interceptor in Options.Interceptors) {
161+
await interceptor.InterceptAfterRequest(responseMessage);
162+
}
133163
}
134164

135165
record HttpResponse(

src/RestSharp/RestClient.Extensions.cs

+12
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,20 @@ public static async Task<RestResponse<T>> ExecuteAsync<T>(
3838
if (request == null) throw new ArgumentNullException(nameof(request));
3939

4040
var response = await client.ExecuteAsync(request, cancellationToken).ConfigureAwait(false);
41+
await OnBeforeDeserialization(response, client.Options).ConfigureAwait(false);
4142
return client.Serializers.Deserialize<T>(request, response, client.Options);
4243
}
44+
45+
/// <summary>
46+
/// Will be called before the Data will be serialized
47+
/// </summary>
48+
/// <param name="raw">RestResponse with Data still in Content</param>
49+
/// <param name="options">RestClient options but readonly</param>
50+
static async Task OnBeforeDeserialization(RestResponse raw, ReadOnlyRestClientOptions options) {
51+
foreach (var interceptor in options.Interceptors) {
52+
await interceptor.InterceptBeforeDeserialize(raw);
53+
}
54+
}
4355

4456
/// <summary>
4557
/// Executes the request synchronously, authenticating if needed

src/RestSharp/Serializers/RestSerializers.cs

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ internal RestResponse<T> Deserialize<T>(RestRequest request, RestResponse raw, R
5353

5454
return response;
5555
}
56+
5657

5758
/// <summary>
5859
/// Deserialize the response content into the specified type
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright (c) .NET Foundation and Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
16+
using Moq;
17+
using RestSharp.Tests.Integrated.Server;
18+
19+
namespace RestSharp.Tests.Integrated.Interceptor;
20+
21+
[Collection(nameof(TestServerCollection))]
22+
public class InterceptorTests {
23+
readonly RestClient _client;
24+
25+
public InterceptorTests(TestServerFixture fixture) => _client = new RestClient(fixture.Server.Url);
26+
27+
[Fact]
28+
public async Task AddInterceptor_ShouldBeUsed() {
29+
//Arrange
30+
var body = new TestRequest("foo", 100);
31+
var request = new RestRequest("post/json").AddJsonBody(body);
32+
33+
var mockInterceptor = new Mock<Interceptors.Interceptor>();
34+
var interceptor = mockInterceptor.Object;
35+
var options = _client.Options;
36+
options.Interceptors.Add(interceptor);
37+
//Act
38+
var response = await _client.ExecutePostAsync<TestResponse>(request);
39+
//Assert
40+
mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny<RestRequest>()));
41+
mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny<HttpRequestMessage>()));
42+
mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny<HttpResponseMessage>()));
43+
mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny<RestResponse>()));
44+
}
45+
[Fact]
46+
public async Task ThrowExceptionIn_InterceptBeforeSerialization_ShouldBeCatchedInTest() {
47+
//Arrange
48+
var body = new TestRequest("foo", 100);
49+
var request = new RestRequest("post/json").AddJsonBody(body);
50+
51+
var mockInterceptor = new Mock<Interceptors.Interceptor>();
52+
mockInterceptor.Setup(m => m.InterceptBeforeSerialization(It.IsAny<RestRequest>())).Throws<Exception>(() => throw new Exception("DummyException"));
53+
var interceptor = mockInterceptor.Object;
54+
var options = _client.Options;
55+
options.Interceptors.Add(interceptor);
56+
//Act
57+
var action = () => _client.ExecutePostAsync<TestResponse>(request);
58+
//Assert
59+
await action.Should().ThrowAsync<Exception>().WithMessage("DummyException");
60+
mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny<RestRequest>()));
61+
mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny<HttpRequestMessage>()),Times.Never);
62+
mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny<HttpResponseMessage>()),Times.Never);
63+
mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny<RestResponse>()),Times.Never);
64+
}
65+
[Fact]
66+
public async Task ThrowExceptionIn_InterceptBeforeRequest_ShouldBeCatchableInTest() {
67+
//Arrange
68+
var body = new TestRequest("foo", 100);
69+
var request = new RestRequest("post/json").AddJsonBody(body);
70+
71+
var mockInterceptor = new Mock<Interceptors.Interceptor>();
72+
mockInterceptor.Setup(m => m.InterceptBeforeRequest(It.IsAny<HttpRequestMessage>())).Throws<Exception>(() => throw new Exception("DummyException"));
73+
var interceptor = mockInterceptor.Object;
74+
var options = _client.Options;
75+
options.Interceptors.Add(interceptor);
76+
//Act
77+
var action = () => _client.ExecutePostAsync<TestResponse>(request);
78+
//Assert
79+
await action.Should().ThrowAsync<Exception>().WithMessage("DummyException");
80+
mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny<RestRequest>()));
81+
mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny<HttpRequestMessage>()));
82+
mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny<HttpResponseMessage>()),Times.Never);
83+
mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny<RestResponse>()),Times.Never);
84+
}
85+
[Fact]
86+
public async Task ThrowExceptionIn_InterceptAfterRequest_ShouldBeCatchableInTest() {
87+
//Arrange
88+
var body = new TestRequest("foo", 100);
89+
var request = new RestRequest("post/json").AddJsonBody(body);
90+
91+
var mockInterceptor = new Mock<Interceptors.Interceptor>();
92+
mockInterceptor.Setup(m => m.InterceptAfterRequest(It.IsAny<HttpResponseMessage>())).Throws<Exception>(() => throw new Exception("DummyException"));
93+
var interceptor = mockInterceptor.Object;
94+
var options = _client.Options;
95+
options.Interceptors.Add(interceptor);
96+
//Act
97+
var action = () => _client.ExecutePostAsync<TestResponse>(request);
98+
//Assert
99+
await action.Should().ThrowAsync<Exception>().WithMessage("DummyException");
100+
mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny<RestRequest>()));
101+
mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny<HttpRequestMessage>()));
102+
mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny<HttpResponseMessage>()));
103+
mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny<RestResponse>()),Times.Never);
104+
}
105+
[Fact]
106+
public async Task ThrowException_InInterceptBeforeDeserialize_ShouldBeCatchableInTest() {
107+
//Arrange
108+
var body = new TestRequest("foo", 100);
109+
var request = new RestRequest("post/json").AddJsonBody(body);
110+
111+
var mockInterceptor = new Mock<Interceptors.Interceptor>();
112+
mockInterceptor.Setup(m => m.InterceptBeforeDeserialize(It.IsAny<RestResponse>())).Throws<Exception>(() => throw new Exception("DummyException"));
113+
var interceptor = mockInterceptor.Object;
114+
var options = _client.Options;
115+
options.Interceptors.Add(interceptor);
116+
//Act
117+
var action = () => _client.PostAsync<TestResponse>(request);
118+
//Assert
119+
await action.Should().ThrowAsync<Exception>().WithMessage("DummyException");
120+
mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny<RestRequest>()));
121+
mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny<HttpRequestMessage>()));
122+
mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny<HttpResponseMessage>()));
123+
mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny<RestResponse>()));
124+
}
125+
126+
127+
}

test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
</ItemGroup>
1717
<ItemGroup>
1818
<PackageReference Include="HttpTracer" Version="2.1.1" />
19+
<PackageReference Include="Moq" Version="4.18.4" />
1920
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="6.0.21" />
2021
<PackageReference Include="Polly" Version="7.2.4" />
2122
<PackageReference Include="Xunit.Extensions.Logging" Version="1.1.0" />

0 commit comments

Comments
 (0)