diff --git a/.gitignore b/.gitignore index 878dbb8d7..311c76314 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ -packages +packages/ +nuget.config + #ignore thumbnails created by windows Thumbs.db diff --git a/RestSharp.sln b/RestSharp.sln index b3efff12e..711c5536c 100644 --- a/RestSharp.sln +++ b/RestSharp.sln @@ -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 diff --git a/gen/SourceGenerator/SourceGenerator.csproj b/gen/SourceGenerator/SourceGenerator.csproj index 29310d2dd..4ff3c5025 100644 --- a/gen/SourceGenerator/SourceGenerator.csproj +++ b/gen/SourceGenerator/SourceGenerator.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 diff --git a/global.json b/global.json new file mode 100644 index 000000000..36e1a9e95 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "7.0.0", + "rollForward": "latestMajor", + "allowPrerelease": false + } +} \ No newline at end of file diff --git a/src/RestSharp/Interceptors/Interceptor.cs b/src/RestSharp/Interceptors/Interceptor.cs new file mode 100644 index 000000000..2d0c016be --- /dev/null +++ b/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; + +/// +/// Base Interceptor +/// +public abstract class Interceptor { + /// + /// Intercepts the request before serialization + /// + /// RestRequest before serialization + /// Value Tags + public virtual ValueTask InterceptBeforeSerialization(RestRequest request) { + return new(); + } + + /// + /// Intercepts the request before being sent + /// + /// HttpRequestMessage before being sent + /// Value Tags + public virtual ValueTask InterceptBeforeRequest(HttpRequestMessage req) { + return new(); + } + + /// + /// Intercepts the request before being sent + /// + /// HttpResponseMessage as received from Server + /// Value Tags + public virtual ValueTask InterceptAfterRequest(HttpResponseMessage responseMessage) { + return new(); + } + + /// + /// Intercepts the request before deserialization + /// + /// HttpResponseMessage as received from Server + /// Value Tags + public virtual ValueTask InterceptBeforeDeserialize(RestResponse response) { + return new(); + } +} \ No newline at end of file diff --git a/src/RestSharp/Options/RestClientOptions.cs b/src/RestSharp/Options/RestClientOptions.cs index 49c197ffc..95db15020 100644 --- a/src/RestSharp/Options/RestClientOptions.cs +++ b/src/RestSharp/Options/RestClientOptions.cs @@ -22,6 +22,7 @@ using System.Text; using RestSharp.Authenticators; using RestSharp.Extensions; +using RestSharp.Interceptors; // ReSharper disable UnusedAutoPropertyAccessor.Global // ReSharper disable PropertyCanBeMadeInitOnly.Global @@ -64,6 +65,8 @@ public RestClientOptions(string baseUrl) : this(new Uri(Ensure.NotEmptyString(ba /// public IAuthenticator? Authenticator { get; set; } + public List Interceptors { get; set; } = new(); + /// /// Passed to Credentials property /// diff --git a/src/RestSharp/RestClient.Async.cs b/src/RestSharp/RestClient.Async.cs index f3b738293..9c99b9fd7 100644 --- a/src/RestSharp/RestClient.Async.cs +++ b/src/RestSharp/RestClient.Async.cs @@ -77,6 +77,7 @@ async Task ExecuteRequestAsync(RestRequest request, CancellationTo throw new ObjectDisposedException(nameof(RestClient)); } + await OnBeforeSerialization(request).ConfigureAwait(false); request.ValidateParameters(); var authenticator = request.Authenticator ?? Options.Authenticator; @@ -98,23 +99,24 @@ async Task ExecuteRequestAsync(RestRequest request, CancellationTo 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 @@ -122,14 +124,42 @@ async Task ExecuteRequestAsync(RestRequest request, CancellationTo // 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); + + } + + /// + /// Will be called before the Request becomes Serialized + /// + /// RestRequest before it will be serialized + async Task OnBeforeSerialization(RestRequest request) { + foreach (var interceptor in Options.Interceptors) { + await interceptor.InterceptBeforeSerialization(request); //.ThrowExceptionIfAvailable(); + } + } + /// + /// Will be called before the Request will be sent + /// + /// HttpRequestMessage ready to be sent + async Task OnBeforeRequest(HttpRequestMessage requestMessage) { + foreach (var interceptor in Options.Interceptors) { + await interceptor.InterceptBeforeRequest(requestMessage); + } + } + /// + /// Will be called after the Response has been received from Server + /// + /// HttpResponseMessage as received from server + async Task OnAfterRequest(HttpResponseMessage responseMessage) { + foreach (var interceptor in Options.Interceptors) { + await interceptor.InterceptAfterRequest(responseMessage); + } } record HttpResponse( diff --git a/src/RestSharp/RestClient.Extensions.cs b/src/RestSharp/RestClient.Extensions.cs index 941db8585..4daede03b 100644 --- a/src/RestSharp/RestClient.Extensions.cs +++ b/src/RestSharp/RestClient.Extensions.cs @@ -38,8 +38,20 @@ public static async Task> ExecuteAsync( 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(request, response, client.Options); } + + /// + /// Will be called before the Data will be serialized + /// + /// RestResponse with Data still in Content + /// RestClient options but readonly + static async Task OnBeforeDeserialization(RestResponse raw, ReadOnlyRestClientOptions options) { + foreach (var interceptor in options.Interceptors) { + await interceptor.InterceptBeforeDeserialize(raw); + } + } /// /// Executes the request synchronously, authenticating if needed diff --git a/src/RestSharp/Serializers/RestSerializers.cs b/src/RestSharp/Serializers/RestSerializers.cs index 00bb8de91..0133db6cf 100644 --- a/src/RestSharp/Serializers/RestSerializers.cs +++ b/src/RestSharp/Serializers/RestSerializers.cs @@ -53,6 +53,7 @@ internal RestResponse Deserialize(RestRequest request, RestResponse raw, R return response; } + /// /// Deserialize the response content into the specified type diff --git a/test/RestSharp.Tests.Integrated/Interceptor/InterceptorTests.cs b/test/RestSharp.Tests.Integrated/Interceptor/InterceptorTests.cs new file mode 100644 index 000000000..3357f5204 --- /dev/null +++ b/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(); + var interceptor = mockInterceptor.Object; + var options = _client.Options; + options.Interceptors.Add(interceptor); + //Act + var response = await _client.ExecutePostAsync(request); + //Assert + mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny())); + mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny())); + mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny())); + mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny())); + } + [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(); + mockInterceptor.Setup(m => m.InterceptBeforeSerialization(It.IsAny())).Throws(() => throw new Exception("DummyException")); + var interceptor = mockInterceptor.Object; + var options = _client.Options; + options.Interceptors.Add(interceptor); + //Act + var action = () => _client.ExecutePostAsync(request); + //Assert + await action.Should().ThrowAsync().WithMessage("DummyException"); + mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny())); + mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny()),Times.Never); + mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny()),Times.Never); + mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny()),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(); + mockInterceptor.Setup(m => m.InterceptBeforeRequest(It.IsAny())).Throws(() => throw new Exception("DummyException")); + var interceptor = mockInterceptor.Object; + var options = _client.Options; + options.Interceptors.Add(interceptor); + //Act + var action = () => _client.ExecutePostAsync(request); + //Assert + await action.Should().ThrowAsync().WithMessage("DummyException"); + mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny())); + mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny())); + mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny()),Times.Never); + mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny()),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(); + mockInterceptor.Setup(m => m.InterceptAfterRequest(It.IsAny())).Throws(() => throw new Exception("DummyException")); + var interceptor = mockInterceptor.Object; + var options = _client.Options; + options.Interceptors.Add(interceptor); + //Act + var action = () => _client.ExecutePostAsync(request); + //Assert + await action.Should().ThrowAsync().WithMessage("DummyException"); + mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny())); + mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny())); + mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny())); + mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny()),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(); + mockInterceptor.Setup(m => m.InterceptBeforeDeserialize(It.IsAny())).Throws(() => throw new Exception("DummyException")); + var interceptor = mockInterceptor.Object; + var options = _client.Options; + options.Interceptors.Add(interceptor); + //Act + var action = () => _client.PostAsync(request); + //Assert + await action.Should().ThrowAsync().WithMessage("DummyException"); + mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny())); + mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny())); + mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny())); + mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny())); + } + + +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj b/test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj index 402bea76b..cd511eb9b 100644 --- a/test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj +++ b/test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj @@ -16,6 +16,7 @@ +