Skip to content

Commit

Permalink
Added Option to add Interceptors on Client Level (#2118)
Browse files Browse the repository at this point in the history
* feature: Added Implementation for Interceptors
  • Loading branch information
fseidl-bauradar committed Sep 13, 2023
1 parent 36ebe2f commit d0b4b18
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 22 deletions.
4 changes: 3 additions & 1 deletion .gitignore
@@ -1,4 +1,6 @@
packages
packages/
nuget.config


#ignore thumbnails created by windows
Thumbs.db
Expand Down
30 changes: 30 additions & 0 deletions RestSharp.sln
Expand Up @@ -446,6 +446,36 @@ Global
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|x64.Build.0 = Release|Any CPU
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|x86.ActiveCfg = Release|Any CPU
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|x86.Build.0 = Release|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|Any CPU.ActiveCfg = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|Any CPU.Build.0 = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|ARM.ActiveCfg = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|ARM.Build.0 = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|Mixed Platforms.ActiveCfg = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|Mixed Platforms.Build.0 = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|x64.ActiveCfg = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|x64.Build.0 = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|x86.ActiveCfg = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug.Appveyor|x86.Build.0 = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|ARM.ActiveCfg = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|ARM.Build.0 = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|x64.ActiveCfg = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|x64.Build.0 = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|x86.ActiveCfg = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Debug|x86.Build.0 = Debug|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|Any CPU.Build.0 = Release|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|ARM.ActiveCfg = Release|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|ARM.Build.0 = Release|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|x64.ActiveCfg = Release|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|x64.Build.0 = Release|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|x86.ActiveCfg = Release|Any CPU
{5E8D472F-5A12-4CD8-8DBE-E3F6E0F76798}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
2 changes: 1 addition & 1 deletion gen/SourceGenerator/SourceGenerator.csproj
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
Expand Down
7 changes: 7 additions & 0 deletions global.json
@@ -0,0 +1,7 @@
{
"sdk": {
"version": "7.0.0",
"rollForward": "latestMajor",
"allowPrerelease": false
}
}
57 changes: 57 additions & 0 deletions src/RestSharp/Interceptors/Interceptor.cs
@@ -0,0 +1,57 @@
// Copyright (c) .NET Foundation and Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

namespace RestSharp.Interceptors;

/// <summary>
/// Base Interceptor
/// </summary>
public abstract class Interceptor {
/// <summary>
/// Intercepts the request before serialization
/// </summary>
/// <param name="request">RestRequest before serialization</param>
/// <returns>Value Tags</returns>
public virtual ValueTask InterceptBeforeSerialization(RestRequest request) {
return new();
}

/// <summary>
/// Intercepts the request before being sent
/// </summary>
/// <param name="req">HttpRequestMessage before being sent</param>
/// <returns>Value Tags</returns>
public virtual ValueTask InterceptBeforeRequest(HttpRequestMessage req) {
return new();
}

/// <summary>
/// Intercepts the request before being sent
/// </summary>
/// <param name="responseMessage">HttpResponseMessage as received from Server</param>
/// <returns>Value Tags</returns>
public virtual ValueTask InterceptAfterRequest(HttpResponseMessage responseMessage) {
return new();
}

/// <summary>
/// Intercepts the request before deserialization
/// </summary>
/// <param name="response">HttpResponseMessage as received from Server</param>
/// <returns>Value Tags</returns>
public virtual ValueTask InterceptBeforeDeserialize(RestResponse response) {
return new();
}
}
3 changes: 3 additions & 0 deletions src/RestSharp/Options/RestClientOptions.cs
Expand Up @@ -22,6 +22,7 @@
using System.Text;
using RestSharp.Authenticators;
using RestSharp.Extensions;
using RestSharp.Interceptors;

// ReSharper disable UnusedAutoPropertyAccessor.Global
// ReSharper disable PropertyCanBeMadeInitOnly.Global
Expand Down Expand Up @@ -64,6 +65,8 @@ public class RestClientOptions {
/// </summary>
public IAuthenticator? Authenticator { get; set; }

public List<Interceptor> Interceptors { get; set; } = new();

/// <summary>
/// Passed to <see cref="HttpMessageHandler"/> <code>Credentials</code> property
/// </summary>
Expand Down
70 changes: 50 additions & 20 deletions src/RestSharp/RestClient.Async.cs
Expand Up @@ -77,6 +77,7 @@ public partial class RestClient {
throw new ObjectDisposedException(nameof(RestClient));
}

await OnBeforeSerialization(request).ConfigureAwait(false);
request.ValidateParameters();
var authenticator = request.Authenticator ?? Options.Authenticator;

Expand All @@ -98,38 +99,67 @@ public partial class RestClient {

var ct = cts.Token;


HttpResponseMessage? responseMessage;
// Make sure we have a cookie container if not provided in the request
CookieContainer cookieContainer = request.CookieContainer ??= new CookieContainer();

var headers = new RequestHeaders()
.AddHeaders(request.Parameters)
.AddHeaders(DefaultParameters)
.AddAcceptHeader(AcceptedContentTypes)
.AddCookieHeaders(url, cookieContainer)
.AddCookieHeaders(url, Options.CookieContainer);

message.AddHeaders(headers);
if (request.OnBeforeRequest != null) await request.OnBeforeRequest(message).ConfigureAwait(false);
await OnBeforeRequest(message).ConfigureAwait(false);

try {
// Make sure we have a cookie container if not provided in the request
var cookieContainer = request.CookieContainer ??= new CookieContainer();

var headers = new RequestHeaders()
.AddHeaders(request.Parameters)
.AddHeaders(DefaultParameters)
.AddAcceptHeader(AcceptedContentTypes)
.AddCookieHeaders(url, cookieContainer)
.AddCookieHeaders(url, Options.CookieContainer);

message.AddHeaders(headers);

if (request.OnBeforeRequest != null) await request.OnBeforeRequest(message).ConfigureAwait(false);

var responseMessage = await HttpClient.SendAsync(message, request.CompletionOption, ct).ConfigureAwait(false);

responseMessage = await HttpClient.SendAsync(message, request.CompletionOption, ct).ConfigureAwait(false);
// Parse all the cookies from the response and update the cookie jar with cookies
if (responseMessage.Headers.TryGetValues(KnownHeaders.SetCookie, out var cookiesHeader)) {
// ReSharper disable once PossibleMultipleEnumeration
cookieContainer.AddCookies(url, cookiesHeader);
// ReSharper disable once PossibleMultipleEnumeration
Options.CookieContainer?.AddCookies(url, cookiesHeader);
}

if (request.OnAfterRequest != null) await request.OnAfterRequest(responseMessage).ConfigureAwait(false);

return new HttpResponse(responseMessage, url, cookieContainer, null, timeoutCts.Token);
}
catch (Exception ex) {
return new HttpResponse(null, url, null, ex, timeoutCts.Token);
}
if (request.OnAfterRequest != null) await request.OnAfterRequest(responseMessage).ConfigureAwait(false);
await OnAfterRequest(responseMessage).ConfigureAwait(false);
return new HttpResponse(responseMessage, url, cookieContainer, null, timeoutCts.Token);

}

/// <summary>
/// Will be called before the Request becomes Serialized
/// </summary>
/// <param name="request">RestRequest before it will be serialized</param>
async Task OnBeforeSerialization(RestRequest request) {
foreach (var interceptor in Options.Interceptors) {
await interceptor.InterceptBeforeSerialization(request); //.ThrowExceptionIfAvailable();
}
}
/// <summary>
/// Will be called before the Request will be sent
/// </summary>
/// <param name="requestMessage">HttpRequestMessage ready to be sent</param>
async Task OnBeforeRequest(HttpRequestMessage requestMessage) {
foreach (var interceptor in Options.Interceptors) {
await interceptor.InterceptBeforeRequest(requestMessage);
}
}
/// <summary>
/// Will be called after the Response has been received from Server
/// </summary>
/// <param name="responseMessage">HttpResponseMessage as received from server</param>
async Task OnAfterRequest(HttpResponseMessage responseMessage) {
foreach (var interceptor in Options.Interceptors) {
await interceptor.InterceptAfterRequest(responseMessage);
}
}

record HttpResponse(
Expand Down
12 changes: 12 additions & 0 deletions src/RestSharp/RestClient.Extensions.cs
Expand Up @@ -38,8 +38,20 @@ public static RestResponse<T> Deserialize<T>(this IRestClient client, RestRespon
if (request == null) throw new ArgumentNullException(nameof(request));

var response = await client.ExecuteAsync(request, cancellationToken).ConfigureAwait(false);
await OnBeforeDeserialization(response, client.Options).ConfigureAwait(false);
return client.Serializers.Deserialize<T>(request, response, client.Options);
}

/// <summary>
/// Will be called before the Data will be serialized
/// </summary>
/// <param name="raw">RestResponse with Data still in Content</param>
/// <param name="options">RestClient options but readonly</param>
static async Task OnBeforeDeserialization(RestResponse raw, ReadOnlyRestClientOptions options) {
foreach (var interceptor in options.Interceptors) {
await interceptor.InterceptBeforeDeserialize(raw);
}
}

/// <summary>
/// Executes the request synchronously, authenticating if needed
Expand Down
1 change: 1 addition & 0 deletions src/RestSharp/Serializers/RestSerializers.cs
Expand Up @@ -53,6 +53,7 @@ public IRestSerializer GetSerializer(DataFormat dataFormat)

return response;
}


/// <summary>
/// Deserialize the response content into the specified type
Expand Down
127 changes: 127 additions & 0 deletions test/RestSharp.Tests.Integrated/Interceptor/InterceptorTests.cs
@@ -0,0 +1,127 @@
// Copyright (c) .NET Foundation and Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

using Moq;
using RestSharp.Tests.Integrated.Server;

namespace RestSharp.Tests.Integrated.Interceptor;

[Collection(nameof(TestServerCollection))]
public class InterceptorTests {
readonly RestClient _client;

public InterceptorTests(TestServerFixture fixture) => _client = new RestClient(fixture.Server.Url);

[Fact]
public async Task AddInterceptor_ShouldBeUsed() {
//Arrange
var body = new TestRequest("foo", 100);
var request = new RestRequest("post/json").AddJsonBody(body);

var mockInterceptor = new Mock<Interceptors.Interceptor>();
var interceptor = mockInterceptor.Object;
var options = _client.Options;
options.Interceptors.Add(interceptor);
//Act
var response = await _client.ExecutePostAsync<TestResponse>(request);
//Assert
mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny<RestRequest>()));
mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny<HttpRequestMessage>()));
mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny<HttpResponseMessage>()));
mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny<RestResponse>()));
}
[Fact]
public async Task ThrowExceptionIn_InterceptBeforeSerialization_ShouldBeCatchedInTest() {
//Arrange
var body = new TestRequest("foo", 100);
var request = new RestRequest("post/json").AddJsonBody(body);

var mockInterceptor = new Mock<Interceptors.Interceptor>();
mockInterceptor.Setup(m => m.InterceptBeforeSerialization(It.IsAny<RestRequest>())).Throws<Exception>(() => throw new Exception("DummyException"));
var interceptor = mockInterceptor.Object;
var options = _client.Options;
options.Interceptors.Add(interceptor);
//Act
var action = () => _client.ExecutePostAsync<TestResponse>(request);
//Assert
await action.Should().ThrowAsync<Exception>().WithMessage("DummyException");
mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny<RestRequest>()));
mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny<HttpRequestMessage>()),Times.Never);
mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny<HttpResponseMessage>()),Times.Never);
mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny<RestResponse>()),Times.Never);
}
[Fact]
public async Task ThrowExceptionIn_InterceptBeforeRequest_ShouldBeCatchableInTest() {
//Arrange
var body = new TestRequest("foo", 100);
var request = new RestRequest("post/json").AddJsonBody(body);

var mockInterceptor = new Mock<Interceptors.Interceptor>();
mockInterceptor.Setup(m => m.InterceptBeforeRequest(It.IsAny<HttpRequestMessage>())).Throws<Exception>(() => throw new Exception("DummyException"));
var interceptor = mockInterceptor.Object;
var options = _client.Options;
options.Interceptors.Add(interceptor);
//Act
var action = () => _client.ExecutePostAsync<TestResponse>(request);
//Assert
await action.Should().ThrowAsync<Exception>().WithMessage("DummyException");
mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny<RestRequest>()));
mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny<HttpRequestMessage>()));
mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny<HttpResponseMessage>()),Times.Never);
mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny<RestResponse>()),Times.Never);
}
[Fact]
public async Task ThrowExceptionIn_InterceptAfterRequest_ShouldBeCatchableInTest() {
//Arrange
var body = new TestRequest("foo", 100);
var request = new RestRequest("post/json").AddJsonBody(body);

var mockInterceptor = new Mock<Interceptors.Interceptor>();
mockInterceptor.Setup(m => m.InterceptAfterRequest(It.IsAny<HttpResponseMessage>())).Throws<Exception>(() => throw new Exception("DummyException"));
var interceptor = mockInterceptor.Object;
var options = _client.Options;
options.Interceptors.Add(interceptor);
//Act
var action = () => _client.ExecutePostAsync<TestResponse>(request);
//Assert
await action.Should().ThrowAsync<Exception>().WithMessage("DummyException");
mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny<RestRequest>()));
mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny<HttpRequestMessage>()));
mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny<HttpResponseMessage>()));
mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny<RestResponse>()),Times.Never);
}
[Fact]
public async Task ThrowException_InInterceptBeforeDeserialize_ShouldBeCatchableInTest() {
//Arrange
var body = new TestRequest("foo", 100);
var request = new RestRequest("post/json").AddJsonBody(body);

var mockInterceptor = new Mock<Interceptors.Interceptor>();
mockInterceptor.Setup(m => m.InterceptBeforeDeserialize(It.IsAny<RestResponse>())).Throws<Exception>(() => throw new Exception("DummyException"));
var interceptor = mockInterceptor.Object;
var options = _client.Options;
options.Interceptors.Add(interceptor);
//Act
var action = () => _client.PostAsync<TestResponse>(request);
//Assert
await action.Should().ThrowAsync<Exception>().WithMessage("DummyException");
mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny<RestRequest>()));
mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny<HttpRequestMessage>()));
mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny<HttpResponseMessage>()));
mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny<RestResponse>()));
}


}
Expand Up @@ -16,6 +16,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="HttpTracer" Version="2.1.1" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="6.0.21" />
<PackageReference Include="Polly" Version="7.2.4" />
<PackageReference Include="Xunit.Extensions.Logging" Version="1.1.0" />
Expand Down

0 comments on commit d0b4b18

Please sign in to comment.