Skip to content

Commit

Permalink
- Added compatibility interceptor
Browse files Browse the repository at this point in the history
- Added docs
- Refactored tests for the hooks
- Updated packages
  • Loading branch information
alexeyzimarev committed Apr 2, 2024
1 parent 78d7d0b commit 74970c2
Show file tree
Hide file tree
Showing 18 changed files with 266 additions and 60 deletions.
52 changes: 28 additions & 24 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,43 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<PropertyGroup Label="Package versions for .NET 6" Condition="$(TargetFramework) == 'net6.0'">
<MicrosoftTestHostVer>[6.0.22,7)</MicrosoftTestHostVer>
<MicrosoftTestHostVer>[6.0.28,7)</MicrosoftTestHostVer>
</PropertyGroup>
<PropertyGroup Label="Package versions for .NET 7" Condition="$(TargetFramework) == 'net7.0'">
<MicrosoftTestHostVer>7.0.11</MicrosoftTestHostVer>
<MicrosoftTestHostVer>7.0.17</MicrosoftTestHostVer>
</PropertyGroup>
<PropertyGroup Label="Package versions for .NET 8" Condition="$(TargetFramework) == 'net8.0'">
<MicrosoftTestHostVer>8.0.0-rc.1.23421.29</MicrosoftTestHostVer>
<MicrosoftTestHostVer>8.0.3</MicrosoftTestHostVer>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="HttpTracer" Version="2.1.1"/>
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftTestHostVer)"/>
<PackageVersion Include="Polly" Version="7.2.4"/>
<PackageVersion Include="Xunit.Extensions.Logging" Version="1.1.0"/>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2"/>
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.0" PrivateAssets="All"/>
<PackageVersion Include="coverlet.collector" Version="6.0.0"/>
<PackageVersion Include="xunit" Version="2.5.0"/>
<PackageVersion Include="AutoFixture" Version="4.18.0"/>
<PackageVersion Include="FluentAssertions" Version="6.12.0"/>
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies.net472" Version="1.0.3"/>
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1"/>
<PackageVersion Include="MinVer" Version="4.3.0"/>
<PackageVersion Include="JetBrains.Annotations" Version="2023.2.0"/>
<ItemGroup Label="Runtime dependencies">
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3"/>
<PackageVersion Include="CsvHelper" Version="30.0.1"/>
<PackageVersion Include="Nullable" Version="1.3.1"/>
<PackageVersion Include="System.Text.Json" Version="7.0.3"/>
<PackageVersion Include="BenchmarkDotNet" Version="0.13.8"/>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0"/>
<PackageVersion Include="System.Text.Json" Version="8.0.3"/>
</ItemGroup>
<ItemGroup Label="Compile dependencies">
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12"/>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2"/>
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4"/>
<PackageVersion Include="MinVer" Version="5.0.0"/>
<PackageVersion Include="Nullable" Version="1.3.1"/>
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies.net472" Version="1.0.3"/>
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0"/>
<PackageVersion Include="JetBrains.Annotations" Version="2023.2.0"/>
</ItemGroup>
<ItemGroup Label="Testing dependencies">
<PackageVersion Include="AutoFixture" Version="4.18.0"/>
<PackageVersion Include="coverlet.collector" Version="6.0.2"/>
<PackageVersion Include="FluentAssertions" Version="6.12.0"/>
<PackageVersion Include="HttpTracer" Version="2.1.1"/>
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftTestHostVer)"/>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0"/>
<PackageVersion Include="Moq" Version="4.20.69"/>
<PackageVersion Include="RichardSzalay.MockHttp" Version="6.0.0"/>
<PackageVersion Include="System.Net.Http.Json" Version="7.0.1"/>
<PackageVersion Include="Polly" Version="8.3.1"/>
<PackageVersion Include="rest-mock-core" Version="0.7.12"/>
<PackageVersion Include="RichardSzalay.MockHttp" Version="7.0.0"/>
<PackageVersion Include="System.Net.Http.Json" Version="8.0.0"/>
<PackageVersion Include="Xunit.Extensions.Logging" Version="1.1.0"/>
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.7" PrivateAssets="All"/>
<PackageVersion Include="xunit" Version="2.7.0"/>
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module.exports = {
"usage.md",
"serialization.md",
"authenticators.md",
"interceptors.md",
"error-handling.md"
]
}
Expand Down
84 changes: 84 additions & 0 deletions docs/interceptors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
title: Interceptors
---

## Intercepting requests and responses

Interceptors are a powerful feature of RestSharp that allows you to modify requests and responses before they are sent or received. You can use interceptors to add headers, modify the request body, or even cancel the request. You can also use interceptors to modify the response before it is returned to the caller.

### Implementing an interceptor

To implement an interceptor, you need to create a class that inherits the `Interceptor` base class. The base class implements all interceptor methods as virtual, so you can override them in your derived class.

Methods that you can override are:
- `BeforeRequest(RestRequest request, CancellationToken cancellationToken)`
- `AfterRequest(RestResponse response, CancellationToken cancellationToken)`
- `BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken)`
- `AfterHttpResponse(HttpResponseMessage responseMessage, CancellationToken cancellationToken)`
- `BeforeDeserialization(RestResponse response, CancellationToken cancellationToken)`

All those functions must return a `ValueTask` instance.

Here's an example of an interceptor that adds a header to a request:

```csharp
// This interceptor adds a header to the request
// You'd not normally use this interceptor, as RestSharp already has a method
// to add headers to the request
class HeaderInterceptor(string headerName, string headerValue) : Interceptors.Interceptor {
public override ValueTask BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken) {
requestMessage.Headers.Add(headerName, headerValue);
return ValueTask.CompletedTask;
}
}
```

Because interceptor functions return `ValueTask`, you can use `async` and `await` inside them.

### Using an interceptor

It's possible to add as many interceptors as you want, both to the client and to the request. The interceptors are executed in the order they were added.

Adding interceptors to the client is done via the client options:

```csharp
var options = new RestClientOptions("https://api.example.com") {
Interceptors = [new HeaderInterceptor("Authorization", token)]
};
var client = new RestClient(options);
```

When you add an interceptor to the client, it will be executed for every request made by that client.

You can also add an interceptor to a specific request:

```csharp
var request = new RestRequest("resource") {
Interceptors = [new HeaderInterceptor("Authorization", token)]
};
```

In this case, the interceptor will only be executed for that specific request.

### Deprecation notice

Interceptors aim to replace the existing request hooks available in RestSharp prior to version 111.0. Those hooks are marked with `Obsolete` attribute and will be removed in the future. If you are using those hooks, we recommend migrating to interceptors as soon as possible.

To make the migration easier, RestSharp provides a class called `CompatibilityInterceptor`. It has properties for the hooks available in RestSharp 110.0 and earlier. You can use it to migrate your code to interceptors without changing the existing logic.

For example, a code that uses `OnBeforeRequest` hook:

```csharp
var request = new RestRequest("success");
request.OnBeforeDeserialization += _ => throw new Exception(exceptionMessage);
```

Can be migrated to interceptors like this:

```csharp
var request = new RestRequest("success") {
Interceptors = [new CompatibilityInterceptor {
OnBeforeDeserialization = _ => throw new Exception(exceptionMessage)
}]
};
```
4 changes: 4 additions & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)' == 'net472' Or '$(TargetFramework)' == 'netstandard2.0'">
<AddSystemTextJson>true</AddSystemTextJson>
<AddNullable>true</AddNullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All"/>
<PackageReference Include="MinVer" PrivateAssets="All"/>
Expand Down
22 changes: 22 additions & 0 deletions src/RestSharp.Serializers.CsvHelper/README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,24 @@
# About

The `RestSharp.Serializers.CsvHelper` library provides a CSV serializer for RestSharp. It is based on the
`CsvHelper` library.

# How to use

Use the extension method provided by the package to configure the client:

```csharp
var client = new RestClient(
options,
configureSerialization: s => s.UseCsvHelper()
);
```

You can also supply your instance of `CsvConfiguration` as a parameter for the extension method.

```csharp
var client = new RestClient(
options,
configureSerialization: s => s.UseCsvHelper(new CsvConfiguration(CultureInfo.InvariantCulture) {...})
);
```
40 changes: 40 additions & 0 deletions src/RestSharp/Interceptors/CompatibilityInterceptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// 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>
/// This class allows easier migration of legacy request hooks to interceptors.
/// </summary>
public class CompatibilityInterceptor : Interceptor {
public Action<RestResponse>? OnBeforeDeserialization { get; set; }
public Func<HttpRequestMessage, ValueTask>? OnBeforeRequest { get; set; }
public Func<HttpResponseMessage, ValueTask>? OnAfterRequest { get; set; }

/// <inheritdoc />
public override ValueTask BeforeDeserialization(RestResponse response, CancellationToken cancellationToken) {
OnBeforeDeserialization?.Invoke(response);
return default;
}

public override ValueTask BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken) {
OnBeforeRequest?.Invoke(requestMessage);
return default;
}

public override ValueTask AfterHttpRequest(HttpResponseMessage responseMessage, CancellationToken cancellationToken) {
OnAfterRequest?.Invoke(responseMessage);
return default;
}
}
5 changes: 0 additions & 5 deletions src/RestSharp/Parameters/FileParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,6 @@ public static FileParameter FromFile(

[PublicAPI]
public class FileParameterOptions {
[Obsolete("Use DisableFilenameStar instead")]
public bool DisableFileNameStar {
get => DisableFilenameStar;
set => DisableFilenameStar = value;
}
public bool DisableFilenameStar { get; set; } = true;
public bool DisableFilenameEncoding { get; set; }
}
4 changes: 2 additions & 2 deletions src/RestSharp/Request/RequestContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ StreamContent ToStreamContent(FileParameter fileParameter) {

HttpContent Serialize(BodyParameter body) {
return body.DataFormat switch {
DataFormat.None => new StringContent(body.Value!.ToString(), client.Options.Encoding, body.ContentType.Value),
DataFormat.None => new StringContent(body.Value!.ToString()!, client.Options.Encoding, body.ContentType.Value),
DataFormat.Binary => GetBinary(),
_ => GetSerialized()
};
Expand Down Expand Up @@ -171,7 +171,7 @@ void AddPostParameters(GetOrPostParameter[] postParameters) {
}
}
else {
var encodedItems = postParameters.Select(x => $"{x.Name!.UrlEncode()}={x.Value?.ToString().UrlEncode() ?? string.Empty}");
var encodedItems = postParameters.Select(x => $"{x.Name!.UrlEncode()}={x.Value?.ToString()?.UrlEncode() ?? string.Empty}");
var encodedContent = new StringContent(encodedItems.JoinToString("&"), client.Options.Encoding, ContentType.FormUrlEncoded.Value);

if (client.Options.DisableCharset) {
Expand Down
2 changes: 1 addition & 1 deletion src/RestSharp/Request/RestRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ public RestRequest(Uri resource, Method method = Method.Get)
[Obsolete("Use Interceptors instead")]
public Func<HttpResponseMessage, ValueTask>? OnAfterRequest { get; set; }

internal void IncreaseNumAttempts() => Attempts++;
internal void IncreaseNumberOfAttempts() => Attempts++;

/// <summary>
/// How many attempts were made to send this Request
Expand Down
2 changes: 1 addition & 1 deletion src/RestSharp/Response/RestResponseBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public abstract class RestResponseBase {
protected RestResponseBase(RestRequest request) {
ResponseStatus = ResponseStatus.None;
Request = request;
Request.IncreaseNumAttempts();
Request.IncreaseNumberOfAttempts();
}

/// <summary>
Expand Down
4 changes: 4 additions & 0 deletions src/RestSharp/RestClient.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
.AddCookieHeaders(url, Options.CookieContainer);

message.AddHeaders(headers);
#pragma warning disable CS0618 // Type or member is obsolete
if (request.OnBeforeRequest != null) await request.OnBeforeRequest(message).ConfigureAwait(false);
#pragma warning restore CS0618 // Type or member is obsolete
await OnBeforeHttpRequest(request, message, cancellationToken).ConfigureAwait(false);

try {
Expand All @@ -148,7 +150,9 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
return new HttpResponse(null, url, null, ex, timeoutCts.Token);
}

#pragma warning disable CS0618 // Type or member is obsolete
if (request.OnAfterRequest != null) await request.OnAfterRequest(responseMessage).ConfigureAwait(false);
#pragma warning restore CS0618 // Type or member is obsolete
await OnAfterHttpRequest(request, responseMessage, cancellationToken).ConfigureAwait(false);
return new HttpResponse(responseMessage, url, cookieContainer, null, timeoutCts.Token);
}
Expand Down
7 changes: 3 additions & 4 deletions src/RestSharp/RestSharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@
<ItemGroup>
<None Remove="RestSharp.csproj.DotSettings"/>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
<PackageReference Include="System.Text.Json"/>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net471'">
<Reference Include="System.Net.Http"/>
<Reference Include="System.Web"/>
<PackageReference Include="Nullable" PrivateAssets="All"/>
<PackageReference Include="System.Text.Json"/>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<ItemGroup Condition="$(AddNullable) == 'true'">
<PackageReference Include="Nullable" PrivateAssets="All"/>
</ItemGroup>
<ItemGroup Condition="$(AddSystemTextJson) == 'true'">
<PackageReference Include="System.Text.Json"/>
</ItemGroup>
<ItemGroup>
Expand Down
15 changes: 9 additions & 6 deletions src/RestSharp/Serializers/RestSerializers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ internal async ValueTask<RestResponse<T>> Deserialize<T>(RestRequest request, Re
var response = RestResponse<T>.FromResponse(raw);

try {
await OnBeforeDeserialization(response, cancellationToken).ConfigureAwait(false);
await OnBeforeDeserialization(raw, cancellationToken).ConfigureAwait(false);
#pragma warning disable CS0618 // Type or member is obsolete
request.OnBeforeDeserialization?.Invoke(raw);
#pragma warning restore CS0618 // Type or member is obsolete
response.Data = DeserializeContent<T>(raw);
}
catch (Exception ex) {
Expand Down Expand Up @@ -80,13 +82,14 @@ static async ValueTask OnBeforeDeserialization(RestResponse response, Cancellati
// This can happen when a request returns for example a 404 page instead of the requested JSON/XML resource
var deserializer = GetContentDeserializer(response);

if (deserializer is IXmlDeserializer xml && response.Request is RestXmlRequest xmlRequest) {
if (xmlRequest.XmlNamespace.IsNotEmpty()) xml.Namespace = xmlRequest.XmlNamespace!;
if (deserializer is not IXmlDeserializer xml || response.Request is not RestXmlRequest xmlRequest)
return deserializer != null ? deserializer.Deserialize<T>(response) : default;

if (xml is IWithDateFormat withDateFormat && xmlRequest.DateFormat.IsNotEmpty()) withDateFormat.DateFormat = xmlRequest.DateFormat!;
}
if (xmlRequest.XmlNamespace.IsNotEmpty()) xml.Namespace = xmlRequest.XmlNamespace!;

if (xml is IWithDateFormat withDateFormat && xmlRequest.DateFormat.IsNotEmpty()) withDateFormat.DateFormat = xmlRequest.DateFormat!;

return deserializer != null ? deserializer.Deserialize<T>(response) : default;
return deserializer.Deserialize<T>(response);
}

IDeserializer? GetContentDeserializer(RestResponseBase response) {
Expand Down
2 changes: 2 additions & 0 deletions test/RestSharp.InteractiveTests/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@

return;

#pragma warning disable CS0162 // Unreachable code detected
var keys = new AuthenticationTests.TwitterKeys {
ConsumerKey = Prompt("Consumer key"),
ConsumerSecret = Prompt("Consumer secret"),
};

await AuthenticationTests.Can_Authenticate_With_OAuth_Async_With_Callback(keys);
#pragma warning restore CS0162 // Unreachable code detected

static string Prompt(string message) {
Console.Write($"{message}: ");
Expand Down
Loading

0 comments on commit 74970c2

Please sign in to comment.