diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index c4e29888f..78f38d41b 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -9,6 +9,13 @@ assignees: ''
**DO NOT USE ISSUES FOR QUESTIONS**
+Note: New issues raised, where it is clear the submitter has not read the issue template,
+are likely to be closed with `invalid` label. Please understand that this is not meant to be rude,
+but to keep the issue list clean and useful for everyone. If one opening the issue decides to ignore the issue template,
+the maintainers might also decide to ignore the issue.
+
+**Remove** all the text above the next line when submitting your issue.
+
**Describe the bug**
A clear and concise description of what the bug is.
Hint: use a tool like https://requestbin.com to compare working and non-working requests.
@@ -22,14 +29,13 @@ A clear and concise description of what you expected to happen.
Post the working request here as well if you made it work using Postman, Swagger, or any other client.
You can use https://requestbin.com/r to create a public request bin and share the link in the issue.
-
**Stack trace**
Copy the full stack trace here if you get an exception.
**Desktop (please complete the following information):**
- OS: [e.g. macOS]
- - .NET version [e.g. .NET 5]
- - Version [e.g. 107.0.4]
+ - .NET version [e.g. .NET 6]
+ - Version [e.g. 110.2.0]
**Additional context**
Add any other context about the problem here.
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index c566a4cc8..1daf73fbb 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -4,24 +4,76 @@ on: [pull_request]
permissions:
contents: read
+ checks: write
jobs:
- test:
+ event_file:
+ name: "Event File"
+ runs-on: ubuntu-latest
+ steps:
+ - name: Upload
+ uses: actions/upload-artifact@v4
+ with:
+ name: Event File
+ path: ${{ github.event_path }}
+ test-windows:
runs-on: windows-latest
+ strategy:
+ matrix:
+ dotnet: ['net48', 'net6.0', 'net7.0', 'net8.0']
+
+ steps:
+ -
+ name: Checkout
+ uses: actions/checkout@v4
+# -
+# name: Setup .NET
+# uses: actions/setup-dotnet@v3
+# with:
+# dotnet-version: '8.0'
+ -
+ name: Run tests
+ run: dotnet test -f ${{ matrix.dotnet }}
+ -
+ name: Upload Test Results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: Test Results Windows ${{ matrix.dotnet }}
+ path: |
+ test-results/**/*.xml
+ test-results/**/*.trx
+ test-results/**/*.json
+
+ test-linux:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ dotnet: ['net6.0', 'net7.0', 'net8.0']
steps:
-
name: Checkout
uses: actions/checkout@v3
- -
- name: Setup .NET
- uses: actions/setup-dotnet@v3
- with:
- dotnet-version: '7.0'
+# -
+# name: Setup .NET
+# uses: actions/setup-dotnet@v3
+# with:
+# dotnet-version: '8.0'
-
name: Run tests
- run: dotnet test -c Release
-
+ run: dotnet test -f ${{ matrix.dotnet }}
+ -
+ name: Upload Test Results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: Test Results Ubuntu ${{ matrix.dotnet }}
+ path: |
+ test-results/**/*.xml
+ test-results/**/*.trx
+ test-results/**/*.json
+
docs:
runs-on: ubuntu-latest
diff --git a/.github/workflows/test-results.yml b/.github/workflows/test-results.yml
new file mode 100644
index 000000000..394bf9dc7
--- /dev/null
+++ b/.github/workflows/test-results.yml
@@ -0,0 +1,37 @@
+name: Test Results
+
+on:
+ workflow_run:
+ workflows: ["Build and test PRs"]
+ types:
+ - completed
+permissions: {}
+
+jobs:
+ test-results:
+ name: Test Results
+ runs-on: ubuntu-latest
+ if: github.event.workflow_run.conclusion != 'skipped'
+
+ permissions:
+ checks: write
+ pull-requests: write
+ actions: read
+
+ steps:
+ -
+ name: Download and Extract Artifacts
+ uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d
+ with:
+ run_id: ${{ github.event.workflow_run.id }}
+ path: artifacts
+ -
+ name: Publish Test Results
+ uses: EnricoMi/publish-unit-test-result-action@v2
+ with:
+ commit: ${{ github.event.workflow_run.head_sha }}
+ event_file: artifacts/Event File/event.json
+ event_name: ${{ github.event.workflow_run.event }}
+ files: |
+ artifacts/**/*.xml
+ artifacts/**/*.trx
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 000000000..df91988b6
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,49 @@
+
+
+ true
+
+
+ [6.0.28,7)
+
+
+ 7.0.17
+
+
+ 8.0.3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RestSharp.sln.DotSettings b/RestSharp.sln.DotSettings
index f22b9a27f..d0bb9d525 100644
--- a/RestSharp.sln.DotSettings
+++ b/RestSharp.sln.DotSettings
@@ -67,6 +67,9 @@
False
FDIC
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" />
+ <Policy><Descriptor Staticness="Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Instance fields (not private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static fields (not private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy>
+ True
True
True
True
@@ -77,6 +80,7 @@
True
True
True
+ True
True
True
True
diff --git a/benchmarks/RestSharp.Benchmarks/RestSharp.Benchmarks.csproj b/benchmarks/RestSharp.Benchmarks/RestSharp.Benchmarks.csproj
index e1da87b9a..f60295a8f 100644
--- a/benchmarks/RestSharp.Benchmarks/RestSharp.Benchmarks.csproj
+++ b/benchmarks/RestSharp.Benchmarks/RestSharp.Benchmarks.csproj
@@ -1,24 +1,25 @@
Exe
- net6.0
+ net7.0
false
- 10
+ preview
enable
+ $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), 'RestSharp.sln'))
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js
index 52aa3bf19..31d27d4b4 100644
--- a/docs/.vuepress/config.js
+++ b/docs/.vuepress/config.js
@@ -27,6 +27,7 @@ module.exports = {
"usage.md",
"serialization.md",
"authenticators.md",
+ "interceptors.md",
"error-handling.md"
]
}
diff --git a/docs/interceptors.md b/docs/interceptors.md
new file mode 100644
index 000000000..86387fb3d
--- /dev/null
+++ b/docs/interceptors.md
@@ -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)
+ }]
+};
+```
\ No newline at end of file
diff --git a/docs/serialization.md b/docs/serialization.md
index a9458523d..cf8d8387b 100644
--- a/docs/serialization.md
+++ b/docs/serialization.md
@@ -37,7 +37,10 @@ In previous versions of RestSharp, the default XML serializer was a custom RestS
You can add it back if necessary by installing the package and adding it to the client:
```csharp
-client.UseXmlSerializer();
+var client = new RestClient(
+ options,
+ configureSerialization: s => s.UseXmlSerializer()
+);
```
As before, you can supply three optional arguments for a custom namespace, custom root element, and if you want to use `SerializeAs` and `DeserializeAs` attributed.
@@ -77,6 +80,29 @@ JsonSerializerSettings DefaultSettings = new JsonSerializerSettings {
If you need to use different settings, you can supply your instance of
`JsonSerializerSettings` as a parameter for the extension method.
+## CSV
+
+A separate package `RestSharp.Serializers.CsvHelper` provides a CSV serializer for RestSharp. It is based on the
+`CsvHelper` library.
+
+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) {...})
+);
+```
+
## Custom
You can also implement your custom serializer. To support both serialization and
diff --git a/gen/SourceGenerator/ImmutableGenerator.cs b/gen/SourceGenerator/ImmutableGenerator.cs
index 367560d68..24adff24d 100644
--- a/gen/SourceGenerator/ImmutableGenerator.cs
+++ b/gen/SourceGenerator/ImmutableGenerator.cs
@@ -58,17 +58,22 @@ static string GenerateImmutableClass(TypeDeclarationSyntax mutableClass, Compila
var mutableProperties = props
.Select(prop => $" {prop.Identifier.Text} = {argName}.{prop.Identifier.Text};");
- var constructor = $@" public ReadOnly{className}({className} {argName}) {{
-{string.Join("\n", mutableProperties)}
- }}";
+ var constructor = $$"""
+ public ReadOnly{{className}}({{className}} {{argName}}) {
+ {{string.Join("\n", mutableProperties)}}
+ CopyAdditionalProperties({{argName}});
+ }
+ """;
const string template = @"{Usings}
namespace {Namespace};
-public class ReadOnly{ClassName} {
+public partial class ReadOnly{ClassName} {
{Constructor}
+ partial void CopyAdditionalProperties({ClassName} {ArgName});
+
{Properties}
}";
@@ -77,6 +82,7 @@ public class ReadOnly{ClassName} {
.Replace("{Namespace}", namespaceName)
.Replace("{ClassName}", className)
.Replace("{Constructor}", constructor)
+ .Replace("{ArgName}", argName)
.Replace("{Properties}", string.Join("\n", properties));
return code;
@@ -86,7 +92,7 @@ IEnumerable GetDefinitions(SyntaxKind kind)
.OfType()
.Where(
prop =>
- prop.AccessorList!.Accessors.Any(accessor => accessor.Keyword.IsKind(kind))
+ prop.AccessorList!.Accessors.Any(accessor => accessor.Keyword.IsKind(kind)) && prop.AttributeLists.All(list => list.Attributes.All(attr => attr.Name.ToString() != "Exclude"))
);
}
}
diff --git a/gen/SourceGenerator/SourceGenerator.csproj b/gen/SourceGenerator/SourceGenerator.csproj
index 4ff3c5025..b4aa74be5 100644
--- a/gen/SourceGenerator/SourceGenerator.csproj
+++ b/gen/SourceGenerator/SourceGenerator.csproj
@@ -1,20 +1,17 @@
-
netstandard2.0
- 11
+ preview
enable
disable
false
true
false
-
-
-
+
+
-
diff --git a/global.json b/global.json
deleted file mode 100644
index 36e1a9e95..000000000
--- a/global.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "sdk": {
- "version": "7.0.0",
- "rollForward": "latestMajor",
- "allowPrerelease": false
- }
-}
\ No newline at end of file
diff --git a/props/Common.props b/props/Common.props
index 26bf7321a..f0ee2b686 100644
--- a/props/Common.props
+++ b/props/Common.props
@@ -3,7 +3,7 @@
$([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.FullName)
$(RepoRoot)\RestSharp.snk
true
- 10
+ preview
enable
enable
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index cc4eccfef..2fa59dd14 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -1,7 +1,7 @@
- netstandard2.0;net471;net6.0;net7.0
+ netstandard2.0;net471;net48;net6.0;net7.0;net8.0
restsharp.png
Apache-2.0
https://restsharp.dev
@@ -15,14 +15,17 @@
snupkg
true
$(NoWarn);1591
- 11
README.md
+
+ true
+ true
+
-
-
-
+
+
+
diff --git a/src/RestSharp.Serializers.CsvHelper/CsvHelperSerializer.cs b/src/RestSharp.Serializers.CsvHelper/CsvHelperSerializer.cs
index 96c19536c..ac4c852c0 100644
--- a/src/RestSharp.Serializers.CsvHelper/CsvHelperSerializer.cs
+++ b/src/RestSharp.Serializers.CsvHelper/CsvHelperSerializer.cs
@@ -5,26 +5,20 @@
namespace RestSharp.Serializers.CsvHelper;
-public class CsvHelperSerializer : IDeserializer, IRestSerializer, ISerializer {
- const string TextCsvContentType = "text/csv";
-
- readonly CsvConfiguration _configuration;
-
+public class CsvHelperSerializer(CsvConfiguration configuration) : IDeserializer, IRestSerializer, ISerializer {
public ISerializer Serializer => this;
public IDeserializer Deserializer => this;
- public string[] AcceptedContentTypes => new[] { TextCsvContentType, "application/x-download" };
+ public string[] AcceptedContentTypes => [ContentType.Csv, "application/x-download"];
public SupportsContentType SupportsContentType => x => Array.IndexOf(AcceptedContentTypes, x) != -1 || x.Value.Contains("csv");
public DataFormat DataFormat => DataFormat.None;
- public ContentType ContentType { get; set; } = TextCsvContentType;
-
- public CsvHelperSerializer() => _configuration = new CsvConfiguration(CultureInfo.InvariantCulture);
+ public ContentType ContentType { get; set; } = ContentType.Csv;
- public CsvHelperSerializer(CsvConfiguration configuration) => _configuration = configuration;
+ public CsvHelperSerializer() : this(new CsvConfiguration(CultureInfo.InvariantCulture)) { }
public T? Deserialize(RestResponse response) {
try {
@@ -33,7 +27,7 @@ public class CsvHelperSerializer : IDeserializer, IRestSerializer, ISerializer {
using var stringReader = new StringReader(response.Content);
- using var csvReader = new CsvReader(stringReader, _configuration);
+ using var csvReader = new CsvReader(stringReader, configuration);
var @interface = typeof(T).GetInterface("IEnumerable`1");
@@ -81,7 +75,7 @@ public class CsvHelperSerializer : IDeserializer, IRestSerializer, ISerializer {
using var stringWriter = new StringWriter();
- using var csvWriter = new CsvWriter(stringWriter, _configuration);
+ using var csvWriter = new CsvWriter(stringWriter, configuration);
if (obj is IEnumerable records) {
csvWriter.WriteRecords(records);
diff --git a/src/RestSharp.Serializers.CsvHelper/README.md b/src/RestSharp.Serializers.CsvHelper/README.md
new file mode 100644
index 000000000..3c7d3815c
--- /dev/null
+++ b/src/RestSharp.Serializers.CsvHelper/README.md
@@ -0,0 +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) {...})
+);
+```
diff --git a/src/RestSharp.Serializers.CsvHelper/RestSharp.Serializers.CsvHelper.csproj b/src/RestSharp.Serializers.CsvHelper/RestSharp.Serializers.CsvHelper.csproj
index 5fff5b301..9c503dd50 100644
--- a/src/RestSharp.Serializers.CsvHelper/RestSharp.Serializers.CsvHelper.csproj
+++ b/src/RestSharp.Serializers.CsvHelper/RestSharp.Serializers.CsvHelper.csproj
@@ -1,6 +1,6 @@
-
+
@@ -8,4 +8,7 @@
+
+
+
diff --git a/src/RestSharp.Serializers.NewtonsoftJson/README.md b/src/RestSharp.Serializers.NewtonsoftJson/README.md
new file mode 100644
index 000000000..5cc11a71b
--- /dev/null
+++ b/src/RestSharp.Serializers.NewtonsoftJson/README.md
@@ -0,0 +1,19 @@
+# About
+
+This library allows using Newtonsoft.Json as a serializer for RestSharp instead of the default JSON serializer based
+on `System.Text.Json`.
+
+# How to use
+
+The default JSON serializer uses `System.Text.Json`, which is a part of .NET since .NET 6.
+
+If you want to use Newtonsoft.Json, you can install the `RestSharp.Serializers.NewtonsoftJson` package and configure
+the
+client to use it:
+
+```csharp
+var client = new RestClient(
+ options,
+ configureSerialization: s => s.UseNewtonsoftJson()
+);
+```
\ No newline at end of file
diff --git a/src/RestSharp.Serializers.NewtonsoftJson/RestSharp.Serializers.NewtonsoftJson.csproj b/src/RestSharp.Serializers.NewtonsoftJson/RestSharp.Serializers.NewtonsoftJson.csproj
index bdc5a5c18..45bcf8ca9 100644
--- a/src/RestSharp.Serializers.NewtonsoftJson/RestSharp.Serializers.NewtonsoftJson.csproj
+++ b/src/RestSharp.Serializers.NewtonsoftJson/RestSharp.Serializers.NewtonsoftJson.csproj
@@ -1,6 +1,6 @@
-
+
@@ -9,4 +9,7 @@
+
+
+
diff --git a/src/RestSharp.Serializers.Xml/README.md b/src/RestSharp.Serializers.Xml/README.md
new file mode 100644
index 000000000..63a7f53cc
--- /dev/null
+++ b/src/RestSharp.Serializers.Xml/README.md
@@ -0,0 +1,22 @@
+# About
+
+This package is a custom XML serializer for RestSharp. It is based on the original XML serializer that was part of RestSharp but was removed in version 107.0.0.
+
+# How to use
+
+The default XML serializer in RestSharp is `DotNetXmlSerializer`, which uses `System.Xml.Serialization` library from .
+NET.
+
+In previous versions of RestSharp, the default XML serializer was a custom RestSharp XML serializer. To make the
+code library size smaller, the custom serializer was removed from RestSharp.
+
+You can add it back if necessary by installing the `RestSharp.Serializers.Xml` package and adding it to the client:
+
+```csharp
+var client = new RestClient(
+ options,
+ configureSerialization: s => s.UseXmlSerializer()
+);
+```
+
+As before, you can supply three optional arguments for a custom namespace, custom root element, and if you want to use `SerializeAs` and `DeserializeAs` attributed.
diff --git a/src/RestSharp.Serializers.Xml/RestSharp.Serializers.Xml.csproj b/src/RestSharp.Serializers.Xml/RestSharp.Serializers.Xml.csproj
index 1d6c8eaab..42ee74306 100644
--- a/src/RestSharp.Serializers.Xml/RestSharp.Serializers.Xml.csproj
+++ b/src/RestSharp.Serializers.Xml/RestSharp.Serializers.Xml.csproj
@@ -8,4 +8,7 @@
+
+
+
diff --git a/src/RestSharp/Authenticators/AuthenticatorBase.cs b/src/RestSharp/Authenticators/AuthenticatorBase.cs
index 1f311dba4..dc765e8c7 100644
--- a/src/RestSharp/Authenticators/AuthenticatorBase.cs
+++ b/src/RestSharp/Authenticators/AuthenticatorBase.cs
@@ -14,10 +14,8 @@
namespace RestSharp.Authenticators;
-public abstract class AuthenticatorBase : IAuthenticator {
- protected AuthenticatorBase(string token) => Token = token;
-
- protected string Token { get; set; }
+public abstract class AuthenticatorBase(string token) : IAuthenticator {
+ protected string Token { get; set; } = token;
protected abstract ValueTask GetAuthenticationParameter(string accessToken);
diff --git a/src/RestSharp/Authenticators/HttpBasicAuthenticator.cs b/src/RestSharp/Authenticators/HttpBasicAuthenticator.cs
index 006ac813f..07d775664 100644
--- a/src/RestSharp/Authenticators/HttpBasicAuthenticator.cs
+++ b/src/RestSharp/Authenticators/HttpBasicAuthenticator.cs
@@ -24,12 +24,10 @@ namespace RestSharp.Authenticators;
/// UTF-8 is used by default but some servers might expect ISO-8859-1 encoding.
///
[PublicAPI]
-public class HttpBasicAuthenticator : AuthenticatorBase {
+public class HttpBasicAuthenticator(string username, string password, Encoding encoding)
+ : AuthenticatorBase(GetHeader(username, password, encoding)) {
public HttpBasicAuthenticator(string username, string password) : this(username, password, Encoding.UTF8) { }
- public HttpBasicAuthenticator(string username, string password, Encoding encoding)
- : base(GetHeader(username, password, encoding)) { }
-
static string GetHeader(string username, string password, Encoding encoding)
=> Convert.ToBase64String(encoding.GetBytes($"{username}:{password}"));
diff --git a/src/RestSharp/Authenticators/JwtAuthenticator.cs b/src/RestSharp/Authenticators/JwtAuthenticator.cs
index 10e8493df..1b90bdd60 100644
--- a/src/RestSharp/Authenticators/JwtAuthenticator.cs
+++ b/src/RestSharp/Authenticators/JwtAuthenticator.cs
@@ -18,9 +18,7 @@ namespace RestSharp.Authenticators;
/// JSON WEB TOKEN (JWT) Authenticator class.
/// https://tools.ietf.org/html/draft-ietf-oauth-json-web-token
///
-public class JwtAuthenticator : AuthenticatorBase {
- public JwtAuthenticator(string accessToken) : base(GetToken(accessToken)) { }
-
+public class JwtAuthenticator(string accessToken) : AuthenticatorBase(GetToken(accessToken)) {
///
/// Set the new bearer token so the request gets the new header value
///
diff --git a/src/RestSharp/Authenticators/OAuth/WebPair.cs b/src/RestSharp/Authenticators/OAuth/WebPair.cs
index d83572e05..381fbbe24 100644
--- a/src/RestSharp/Authenticators/OAuth/WebPair.cs
+++ b/src/RestSharp/Authenticators/OAuth/WebPair.cs
@@ -14,16 +14,10 @@
namespace RestSharp.Authenticators.OAuth;
-class WebPair {
- public WebPair(string name, string? value, bool encode = false) {
- Name = name;
- Value = value;
- WebValue = encode ? OAuthTools.UrlEncodeRelaxed(value) : value;
- }
-
- public string Name { get; }
- public string? Value { get; }
- string? WebValue { get; }
+class WebPair(string name, string? value, bool encode = false) {
+ public string Name { get; } = name;
+ public string? Value { get; } = value;
+ string? WebValue { get; } = encode ? OAuthTools.UrlEncodeRelaxed(value) : value;
public string GetQueryParameter(bool web) {
var value = web ? $"\"{WebValue}\"" : Value;
diff --git a/src/RestSharp/ContentType.cs b/src/RestSharp/ContentType.cs
index 54bec16e8..3a277de69 100644
--- a/src/RestSharp/ContentType.cs
+++ b/src/RestSharp/ContentType.cs
@@ -29,6 +29,7 @@ public class ContentType : IEquatable {
public static readonly ContentType Json = "application/json";
public static readonly ContentType Xml = "application/xml";
public static readonly ContentType Plain = "text/plain";
+ public static readonly ContentType Csv = "text/csv";
public static readonly ContentType Binary = "application/octet-stream";
public static readonly ContentType GZip = "application/x-gzip";
public static readonly ContentType FormUrlEncoded = "application/x-www-form-urlencoded";
@@ -84,4 +85,4 @@ public override bool Equals(object? obj) {
}
public override int GetHashCode() => _value.GetHashCode();
-}
+}
\ No newline at end of file
diff --git a/src/RestSharp/Extensions/GenerateImmutableAttribute.cs b/src/RestSharp/Extensions/GenerateImmutableAttribute.cs
index fcd8c546d..c4fe51817 100644
--- a/src/RestSharp/Extensions/GenerateImmutableAttribute.cs
+++ b/src/RestSharp/Extensions/GenerateImmutableAttribute.cs
@@ -16,4 +16,7 @@
namespace RestSharp.Extensions;
[AttributeUsage(AttributeTargets.Class)]
-public class GenerateImmutableAttribute : Attribute { }
+class GenerateImmutableAttribute : Attribute { }
+
+[AttributeUsage(AttributeTargets.Property)]
+class Exclude : Attribute { }
diff --git a/src/RestSharp/Interceptors/CompatibilityInterceptor.cs b/src/RestSharp/Interceptors/CompatibilityInterceptor.cs
new file mode 100644
index 000000000..f39b2f361
--- /dev/null
+++ b/src/RestSharp/Interceptors/CompatibilityInterceptor.cs
@@ -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;
+
+///
+/// This class allows easier migration of legacy request hooks to interceptors.
+///
+public class CompatibilityInterceptor : Interceptor {
+ public Action? OnBeforeDeserialization { get; set; }
+ public Func? OnBeforeRequest { get; set; }
+ public Func? OnAfterRequest { get; set; }
+
+ ///
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/src/RestSharp/Interceptors/Interceptor.cs b/src/RestSharp/Interceptors/Interceptor.cs
index 2d0c016be..ddd765f52 100644
--- a/src/RestSharp/Interceptors/Interceptor.cs
+++ b/src/RestSharp/Interceptors/Interceptor.cs
@@ -13,45 +13,50 @@
// limitations under the License.
//
-namespace RestSharp.Interceptors;
+namespace RestSharp.Interceptors;
///
/// Base Interceptor
///
public abstract class Interceptor {
+ static readonly ValueTask Completed =
+#if NET
+ ValueTask.CompletedTask;
+#else
+ new ();
+#endif
///
- /// Intercepts the request before serialization
+ /// Intercepts the request before composing the request message
///
- /// RestRequest before serialization
- /// Value Tags
- public virtual ValueTask InterceptBeforeSerialization(RestRequest request) {
- return new();
- }
+ /// RestRequest before composing the request message
+ /// Cancellation token
+ public virtual ValueTask BeforeRequest(RestRequest request, CancellationToken cancellationToken) => Completed;
///
/// Intercepts the request before being sent
///
- /// HttpRequestMessage before being sent
- /// Value Tags
- public virtual ValueTask InterceptBeforeRequest(HttpRequestMessage req) {
- return new();
- }
+ /// HttpRequestMessage before being sent
+ /// Cancellation token
+ public virtual ValueTask BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken) => Completed;
///
/// Intercepts the request before being sent
///
- /// HttpResponseMessage as received from Server
- /// Value Tags
- public virtual ValueTask InterceptAfterRequest(HttpResponseMessage responseMessage) {
- return new();
- }
+ /// HttpResponseMessage as received from the remote server
+ /// Cancellation token
+ public virtual ValueTask AfterHttpRequest(HttpResponseMessage responseMessage, CancellationToken cancellationToken) => Completed;
///
- /// Intercepts the request before deserialization
+ /// Intercepts the request after it's created from HttpResponseMessage
///
- /// HttpResponseMessage as received from Server
- /// Value Tags
- public virtual ValueTask InterceptBeforeDeserialize(RestResponse response) {
- return new();
- }
-}
\ No newline at end of file
+ /// HttpResponseMessage as received from the remote server
+ /// Cancellation token
+ public virtual ValueTask AfterRequest(RestResponse response, CancellationToken cancellationToken) => Completed;
+
+ ///
+ /// Intercepts the request before deserialization, won't be called if using non-generic ExecuteAsync
+ ///
+ /// HttpResponseMessage as received from the remote server
+ /// Cancellation token
+ public virtual ValueTask BeforeDeserialization(RestResponse response, CancellationToken cancellationToken) => Completed;
+}
diff --git a/src/RestSharp/Options/ReadOnlyRestClientOptions.cs b/src/RestSharp/Options/ReadOnlyRestClientOptions.cs
new file mode 100644
index 000000000..abe93af0d
--- /dev/null
+++ b/src/RestSharp/Options/ReadOnlyRestClientOptions.cs
@@ -0,0 +1,32 @@
+// 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 RestSharp.Interceptors;
+
+namespace RestSharp;
+
+public partial class ReadOnlyRestClientOptions {
+ public IReadOnlyCollection? Interceptors { get; private set; }
+
+ // partial void CopyAdditionalProperties(RestClientOptions inner);
+ partial void CopyAdditionalProperties(RestClientOptions inner) => Interceptors = GetInterceptors(inner);
+
+ static IReadOnlyCollection? GetInterceptors(RestClientOptions? options) {
+ if (options == null || options.Interceptors.Count == 0) return null;
+
+ var interceptors = new List(options.Interceptors);
+ return interceptors.AsReadOnly();
+ }
+}
diff --git a/src/RestSharp/Options/RestClientOptions.cs b/src/RestSharp/Options/RestClientOptions.cs
index 95db15020..9b28c66ad 100644
--- a/src/RestSharp/Options/RestClientOptions.cs
+++ b/src/RestSharp/Options/RestClientOptions.cs
@@ -65,6 +65,10 @@ public RestClientOptions(string baseUrl) : this(new Uri(Ensure.NotEmptyString(ba
///
public IAuthenticator? Authenticator { get; set; }
+ ///
+ /// List of interceptors that will be executed before the request is sent
+ ///
+ [Exclude]
public List Interceptors { get; set; } = new();
///
@@ -114,6 +118,7 @@ public RestClientOptions(string baseUrl) : this(new Uri(Ensure.NotEmptyString(ba
#if NET
[UnsupportedOSPlatform("browser")]
#endif
+ [Exclude]
public X509CertificateCollection? ClientCertificates { get; set; }
///
diff --git a/src/RestSharp/Parameters/DefaultParameters.cs b/src/RestSharp/Parameters/DefaultParameters.cs
index 6d3dc89a7..6ceb740fd 100644
--- a/src/RestSharp/Parameters/DefaultParameters.cs
+++ b/src/RestSharp/Parameters/DefaultParameters.cs
@@ -13,15 +13,11 @@
// limitations under the License.
//
-namespace RestSharp;
-
-public sealed class DefaultParameters : ParametersCollection {
- readonly ReadOnlyRestClientOptions _options;
+using System.Runtime.CompilerServices;
- readonly object _lock = new();
-
- public DefaultParameters(ReadOnlyRestClientOptions options) => _options = options;
+namespace RestSharp;
+public sealed class DefaultParameters(ReadOnlyRestClientOptions options) : ParametersCollection {
///
/// Safely add a default parameter to the collection.
///
@@ -29,22 +25,21 @@ public sealed class DefaultParameters : ParametersCollection {
///
///
///
+ [MethodImpl(MethodImplOptions.Synchronized)]
public DefaultParameters AddParameter(Parameter parameter) {
- lock (_lock) {
- if (parameter.Type == ParameterType.RequestBody)
- throw new NotSupportedException(
- "Cannot set request body using default parameters. Use Request.AddBody() instead."
- );
+ if (parameter.Type == ParameterType.RequestBody)
+ throw new NotSupportedException(
+ "Cannot set request body using default parameters. Use Request.AddBody() instead."
+ );
- if (!_options.AllowMultipleDefaultParametersWithSameName &&
- !MultiParameterTypes.Contains(parameter.Type) &&
- this.Any(x => x.Name == parameter.Name)) {
- throw new ArgumentException("A default parameters with the same name has already been added", nameof(parameter));
- }
-
- Parameters.Add(parameter);
+ if (!options.AllowMultipleDefaultParametersWithSameName &&
+ !MultiParameterTypes.Contains(parameter.Type) &&
+ this.Any(x => x.Name == parameter.Name)) {
+ throw new ArgumentException("A default parameters with the same name has already been added", nameof(parameter));
}
+ Parameters.Add(parameter);
+
return this;
}
@@ -55,10 +50,9 @@ public DefaultParameters AddParameter(Parameter parameter) {
/// Parameter type
///
[PublicAPI]
+ [MethodImpl(MethodImplOptions.Synchronized)]
public DefaultParameters RemoveParameter(string name, ParameterType type) {
- lock (_lock) {
- Parameters.RemoveAll(x => x.Name == name && x.Type == type);
- }
+ Parameters.RemoveAll(x => x.Name == name && x.Type == type);
return this;
}
diff --git a/src/RestSharp/Parameters/FileParameter.cs b/src/RestSharp/Parameters/FileParameter.cs
index 5b58bf44d..dfb4d25d1 100644
--- a/src/RestSharp/Parameters/FileParameter.cs
+++ b/src/RestSharp/Parameters/FileParameter.cs
@@ -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; }
}
diff --git a/src/RestSharp/Parameters/ObjectParser.cs b/src/RestSharp/Parameters/ObjectParser.cs
index fec2a0a85..52ab89865 100644
--- a/src/RestSharp/Parameters/ObjectParser.cs
+++ b/src/RestSharp/Parameters/ObjectParser.cs
@@ -36,8 +36,6 @@ public static IEnumerable GetProperties(this object obj, params
properties.Add(GetValue(prop, val));
}
- string? ParseValue(string? format, object? value) => format == null ? value?.ToString() : string.Format($"{{0:{format}}}", value);
-
IEnumerable GetArray(PropertyInfo propertyInfo, object? value) {
var elementType = propertyInfo.PropertyType.GetElementType();
var array = (Array)value!;
@@ -47,20 +45,19 @@ IEnumerable GetArray(PropertyInfo propertyInfo, object? value)
var queryType = attribute?.ArrayQueryType ?? RequestArrayQueryType.CommaSeparated;
var encode = attribute?.Encode ?? true;
- if (array.Length > 0 && elementType != null) {
- // convert the array to an array of strings
- var values = array
- .Cast
diff --git a/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedSimpleTests.cs b/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedSimpleTests.cs
index 214d94795..573a04f52 100644
--- a/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedSimpleTests.cs
+++ b/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedSimpleTests.cs
@@ -1,32 +1,24 @@
-using System.Net;
-using System.Text;
using RestSharp.Serializers.NewtonsoftJson;
-using RestSharp.Tests.Shared.Extensions;
using RestSharp.Tests.Shared.Fixtures;
namespace RestSharp.Tests.Serializers.Json.NewtonsoftJson;
-public class IntegratedSimpleTests {
- string _body;
-
- void CaptureBody(HttpListenerRequest request, HttpListenerResponse response) => _body = request.InputStream.StreamToString();
-
+public sealed class IntegratedSimpleTests : IDisposable {
static readonly Fixture Fixture = new();
+ readonly WireMockServer _server = WireMockServer.Start();
+
[Fact]
public async Task Use_JsonNet_For_Requests() {
- using var server = HttpServerFixture.StartServer(CaptureBody);
- _body = null;
+ var capturer = _server.ConfigureBodyCapturer(Method.Post, false);
var serializer = new JsonNetSerializer();
-
- var testData = Fixture.Create();
-
- var client = new RestClient(server.Url, configureSerialization: cfg => cfg.UseNewtonsoftJson());
- var request = new RestRequest().AddJsonBody(testData);
+ var testData = Fixture.Create();
+ var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseNewtonsoftJson());
+ var request = new RestRequest().AddJsonBody(testData);
await client.PostAsync(request);
- var actual = serializer.Deserialize(new RestResponse(request) { Content = _body! });
+ var actual = serializer.Deserialize(new RestResponse(request) { Content = capturer.Body! });
actual.Should().BeEquivalentTo(testData);
}
@@ -34,19 +26,11 @@ public async Task Use_JsonNet_For_Requests() {
[Fact]
public async Task Use_JsonNet_For_Response() {
var expected = Fixture.Create();
+ _server
+ .Given(Request.Create().WithPath("/").UsingGet())
+ .RespondWith(Response.Create().WithBodyAsJson(expected));
- using var server = HttpServerFixture.StartServer(
- (_, response) => {
- var serializer = new JsonNetSerializer();
-
- response.ContentType = "application/json";
- response.ContentEncoding = Encoding.UTF8;
- response.OutputStream.WriteStringUtf8(serializer.Serialize(expected)!);
- }
- );
-
- var client = new RestClient(server.Url, configureSerialization: cfg => cfg.UseNewtonsoftJson());
-
+ var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseNewtonsoftJson());
var actual = await client.GetAsync(new RestRequest());
actual.Should().BeEquivalentTo(expected);
@@ -54,16 +38,11 @@ public async Task Use_JsonNet_For_Response() {
[Fact]
public async Task DeserilizationFails_IsSuccessful_Should_BeFalse() {
- using var server = HttpServerFixture.StartServer(
- (_, response) => {
- response.StatusCode = (int)HttpStatusCode.OK;
- response.ContentType = "application/json";
- response.ContentEncoding = Encoding.UTF8;
- response.OutputStream.WriteStringUtf8("invalid json");
- }
- );
+ _server
+ .Given(Request.Create().WithPath("/").UsingGet())
+ .RespondWith(Response.Create().WithBody("invalid json").WithHeader(KnownHeaders.ContentType, ContentType.Json));
- var client = new RestClient(server.Url, configureSerialization: cfg => cfg.UseNewtonsoftJson());
+ var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseNewtonsoftJson());
var response = await client.ExecuteAsync(new RestRequest());
@@ -74,23 +53,17 @@ public async Task DeserilizationFails_IsSuccessful_Should_BeFalse() {
[Fact]
public async Task DeserilizationSucceeds_IsSuccessful_Should_BeTrue() {
var item = Fixture.Create();
+ _server
+ .Given(Request.Create().WithPath("/").UsingGet())
+ .RespondWith(Response.Create().WithBodyAsJson(item));
- using var server = HttpServerFixture.StartServer(
- (_, response) => {
- var serializer = new JsonNetSerializer();
-
- response.StatusCode = (int)HttpStatusCode.OK;
- response.ContentType = "application/json";
- response.ContentEncoding = Encoding.UTF8;
- response.OutputStream.WriteStringUtf8(serializer.Serialize(item)!);
- }
- );
-
- var client = new RestClient(server.Url, configureSerialization: cfg => cfg.UseNewtonsoftJson());
+ var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseNewtonsoftJson());
var response = await client.ExecuteAsync(new RestRequest());
response.IsSuccessStatusCode.Should().BeTrue();
response.IsSuccessful.Should().BeTrue();
}
-}
+
+ public void Dispose() => _server?.Dispose();
+}
\ No newline at end of file
diff --git a/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedTests.cs b/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedTests.cs
index ce74b3a86..9fcd45865 100644
--- a/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedTests.cs
+++ b/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedTests.cs
@@ -1,25 +1,24 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
-using RestMockCore;
using RestSharp.Serializers.NewtonsoftJson;
namespace RestSharp.Tests.Serializers.Json.NewtonsoftJson;
-public class IntegratedTests {
+public sealed class IntegratedTests : IDisposable {
static readonly Fixture Fixture = new();
- const int Port = 5001;
+ readonly WireMockServer _server = WireMockServer.Start();
[Fact]
public async Task Use_with_GetJsonAsync() {
var data = Fixture.Create();
var serialized = JsonConvert.SerializeObject(data, JsonNetSerializer.DefaultSettings);
- using var server = new HttpServer(Port);
- server.Config.Get("/test").Send(serialized);
- server.Run();
+ _server
+ .Given(Request.Create().WithPath("/test").UsingGet())
+ .RespondWith(Response.Create().WithBody(serialized).WithHeader(KnownHeaders.ContentType, ContentType.Json));
- using var client = new RestClient($"http://localhost:{Port}", configureSerialization: cfg => cfg.UseNewtonsoftJson());
+ using var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseNewtonsoftJson());
var response = await client.GetJsonAsync("/test");
@@ -34,14 +33,16 @@ public async Task Use_with_GetJsonAsync_custom_settings() {
var data = Fixture.Create();
var serialized = JsonConvert.SerializeObject(data, settings);
- using var server = new HttpServer(Port);
- server.Config.Get("/test").Send(serialized);
- server.Run();
+ _server
+ .Given(Request.Create().WithPath("/test").UsingGet())
+ .RespondWith(Response.Create().WithBody(serialized).WithHeader(KnownHeaders.ContentType, ContentType.Json));
- using var client = new RestClient($"http://localhost:{Port}", configureSerialization: cfg => cfg.UseNewtonsoftJson(settings));
+ using var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseNewtonsoftJson(settings));
var response = await client.GetJsonAsync("/test");
response.Should().BeEquivalentTo(data);
}
+
+ public void Dispose() => _server?.Dispose();
}
diff --git a/test/RestSharp.Tests.Serializers.Json/RestSharp.Tests.Serializers.Json.csproj b/test/RestSharp.Tests.Serializers.Json/RestSharp.Tests.Serializers.Json.csproj
index a7703166c..ef87290b5 100644
--- a/test/RestSharp.Tests.Serializers.Json/RestSharp.Tests.Serializers.Json.csproj
+++ b/test/RestSharp.Tests.Serializers.Json/RestSharp.Tests.Serializers.Json.csproj
@@ -1,18 +1,16 @@
-
-
-
+
+
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/test/RestSharp.Tests.Serializers.Json/SystemTextJson/SystemTextJsonTests.cs b/test/RestSharp.Tests.Serializers.Json/SystemTextJson/SystemTextJsonTests.cs
index 5d5976d48..5cfd6ac5d 100644
--- a/test/RestSharp.Tests.Serializers.Json/SystemTextJson/SystemTextJsonTests.cs
+++ b/test/RestSharp.Tests.Serializers.Json/SystemTextJson/SystemTextJsonTests.cs
@@ -1,51 +1,37 @@
-using System.Net;
-using System.Text;
using RestSharp.Serializers.Json;
-using RestSharp.Tests.Shared.Extensions;
using RestSharp.Tests.Shared.Fixtures;
namespace RestSharp.Tests.Serializers.Json.SystemTextJson;
-public class SystemTextJsonTests {
+public sealed class SystemTextJsonTests : IDisposable {
static readonly Fixture Fixture = new();
- string _body;
-
+ readonly WireMockServer _server = WireMockServer.Start();
+
[Fact]
public async Task Use_JsonNet_For_Requests() {
- using var server = HttpServerFixture.StartServer(CaptureBody);
- _body = null;
var serializer = new SystemTextJsonSerializer();
+ var capturer = _server.ConfigureBodyCapturer(Method.Post, false);
var testData = Fixture.Create();
-
- var client = new RestClient(server.Url);
+ var client = new RestClient(_server.Url!);
var request = new RestRequest().AddJsonBody(testData);
await client.PostAsync(request);
- var actual = serializer.Deserialize(new RestResponse(request) { Content = _body });
+ var actual = serializer.Deserialize(new RestResponse(request) { Content = capturer.Body });
actual.Should().BeEquivalentTo(testData);
-
- void CaptureBody(HttpListenerRequest req, HttpListenerResponse response) => _body = req.InputStream.StreamToString();
}
[Fact]
public async Task Use_JsonNet_For_Response() {
var expected = Fixture.Create();
+ _server
+ .Given(Request.Create().WithPath("/").UsingGet())
+ .RespondWith(Response.Create().WithBodyAsJson(expected));
- using var server = HttpServerFixture.StartServer(
- (_, response) => {
- var serializer = new SystemTextJsonSerializer();
-
- response.ContentType = "application/json";
- response.ContentEncoding = Encoding.UTF8;
- response.OutputStream.WriteStringUtf8(serializer.Serialize(expected)!);
- }
- );
-
- var client = new RestClient(server.Url, configureSerialization: cfg => cfg.UseSystemTextJson());
+ var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseSystemTextJson());
var actual = await client.GetAsync(new RestRequest());
@@ -54,16 +40,11 @@ public async Task Use_JsonNet_For_Response() {
[Fact]
public async Task DeserilizationFails_IsSuccessful_Should_BeFalse() {
- using var server = HttpServerFixture.StartServer(
- (_, response) => {
- response.StatusCode = (int)HttpStatusCode.OK;
- response.ContentType = "application/json";
- response.ContentEncoding = Encoding.UTF8;
- response.OutputStream.WriteStringUtf8("invalid json");
- }
- );
+ _server
+ .Given(Request.Create().WithPath("/").UsingGet())
+ .RespondWith(Response.Create().WithBody("invalid json").WithHeader(KnownHeaders.ContentType, ContentType.Json));
- var client = new RestClient(server.Url, configureSerialization: cfg => cfg.UseSystemTextJson());
+ var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseSystemTextJson());
var response = await client.ExecuteAsync(new RestRequest());
@@ -74,23 +55,17 @@ public async Task DeserilizationFails_IsSuccessful_Should_BeFalse() {
[Fact]
public async Task DeserilizationSucceeds_IsSuccessful_Should_BeTrue() {
var item = Fixture.Create();
+ _server
+ .Given(Request.Create().WithPath("/").UsingGet())
+ .RespondWith(Response.Create().WithBodyAsJson(item));
- using var server = HttpServerFixture.StartServer(
- (_, response) => {
- var serializer = new SystemTextJsonSerializer();
-
- response.StatusCode = (int)HttpStatusCode.OK;
- response.ContentType = "application/json";
- response.ContentEncoding = Encoding.UTF8;
- response.OutputStream.WriteStringUtf8(serializer.Serialize(item)!);
- }
- );
-
- var client = new RestClient(server.Url, configureSerialization: cfg => cfg.UseSystemTextJson());
+ var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseSystemTextJson());
var response = await client.ExecuteAsync(new RestRequest());
response.IsSuccessStatusCode.Should().BeTrue();
response.IsSuccessful.Should().BeTrue();
}
+
+ public void Dispose() => _server?.Dispose();
}
diff --git a/test/RestSharp.Tests.Serializers.Xml/NamespacedXmlTests.cs b/test/RestSharp.Tests.Serializers.Xml/NamespacedXmlTests.cs
index 8b4ad7809..4d3a32b64 100644
--- a/test/RestSharp.Tests.Serializers.Xml/NamespacedXmlTests.cs
+++ b/test/RestSharp.Tests.Serializers.Xml/NamespacedXmlTests.cs
@@ -63,7 +63,7 @@ static string CreateUnderscoresXml() {
foes.Add(new XAttribute(ns + "Team", "Yankees"));
- for (var i = 0; i < 5; i++) foes.Add(new XElement(ns + "Foe", new XElement(ns + "Nickname", "Foe" + i)));
+ for (var i = 0; i < 5; i++) foes.Add(new XElement(ns + "Foe", new XElement(ns + "Nickname", $"Foe{i}")));
root.Add(foes);
doc.Add(root);
diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/twitter.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/twitter.cs
index df64ad212..cd02cc6ad 100644
--- a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/twitter.cs
+++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/twitter.cs
@@ -1,6 +1,8 @@
using RestSharp.Serializers;
// ReSharper disable InconsistentNaming
// ReSharper disable UnusedMember.Global
+#pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language.
+#pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language.
#pragma warning disable CS8981
namespace RestSharp.Tests.Serializers.Xml.SampleClasses;
diff --git a/test/RestSharp.Tests.Serializers.Xml/XmlAttributeDeserializerTests.cs b/test/RestSharp.Tests.Serializers.Xml/XmlAttributeDeserializerTests.cs
index 9f6277c6f..ca07e7b53 100644
--- a/test/RestSharp.Tests.Serializers.Xml/XmlAttributeDeserializerTests.cs
+++ b/test/RestSharp.Tests.Serializers.Xml/XmlAttributeDeserializerTests.cs
@@ -6,11 +6,9 @@
namespace RestSharp.Tests.Serializers.Xml;
public class XmlAttributeDeserializerTests {
- readonly ITestOutputHelper _output;
-
const string GuidString = "AC1FC4BC-087A-4242-B8EE-C53EBE9887A5";
- #if NETCORE
+ #if NET
readonly string _sampleDataPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SampleData");
#else
readonly string _sampleDataPath = Path.Combine(Directory.GetCurrentDirectory(), "SampleData");
@@ -18,8 +16,6 @@ public class XmlAttributeDeserializerTests {
string PathFor(string sampleFile) => Path.Combine(_sampleDataPath, sampleFile);
- public XmlAttributeDeserializerTests(ITestOutputHelper output) => _output = output;
-
[Fact]
public void Can_Deserialize_Lists_of_Simple_Types() {
var xmlPath = PathFor("xmllists.xml");
diff --git a/test/RestSharp.Tests.Serializers.Xml/XmlDeserializerTests.cs b/test/RestSharp.Tests.Serializers.Xml/XmlDeserializerTests.cs
index c1e39af0f..f9c680b3a 100644
--- a/test/RestSharp.Tests.Serializers.Xml/XmlDeserializerTests.cs
+++ b/test/RestSharp.Tests.Serializers.Xml/XmlDeserializerTests.cs
@@ -9,7 +9,7 @@ namespace RestSharp.Tests.Serializers.Xml;
public class XmlDeserializerTests {
const string GuidString = "AC1FC4BC-087A-4242-B8EE-C53EBE9887A5";
-#if NETCORE
+#if NET
readonly string _sampleDataPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SampleData");
#else
readonly string _sampleDataPath = Path.Combine(Directory.GetCurrentDirectory(), "SampleData");
diff --git a/test/RestSharp.Tests.Shared/Fixtures/Handlers.cs b/test/RestSharp.Tests.Shared/Fixtures/Handlers.cs
index 8183d4607..6aa76d27b 100644
--- a/test/RestSharp.Tests.Shared/Fixtures/Handlers.cs
+++ b/test/RestSharp.Tests.Shared/Fixtures/Handlers.cs
@@ -1,20 +1,9 @@
using System.Net;
using System.Reflection;
-using RestSharp.Tests.Shared.Extensions;
namespace RestSharp.Tests.Shared.Fixtures;
public static class Handlers {
- ///
- /// Echoes the request input back to the output.
- ///
- public static void Echo(HttpListenerContext context) => context.Request.InputStream.CopyTo(context.Response.OutputStream);
-
- ///
- /// Echoes the given value back to the output.
- ///
- public static Action EchoValue(string value) => ctx => ctx.Response.OutputStream.WriteStringUtf8(value);
-
///
/// T should be a class that implements methods whose names match the urls being called, and take one parameter, an
/// HttpListenerContext.
diff --git a/test/RestSharp.Tests.Shared/Fixtures/HttpServerFixture.cs b/test/RestSharp.Tests.Shared/Fixtures/HttpServerFixture.cs
deleted file mode 100644
index d94595845..000000000
--- a/test/RestSharp.Tests.Shared/Fixtures/HttpServerFixture.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.Net;
-
-namespace RestSharp.Tests.Shared.Fixtures;
-
-public sealed class HttpServerFixture : IDisposable {
- public static HttpServerFixture StartServer(string url, Action handle) {
- var server = new TestHttpServer(0, url, (request, response, _) => handle(request, response));
- return new HttpServerFixture(server);
- }
-
- public static HttpServerFixture StartServer(Action handle) => StartServer("", handle);
-
- HttpServerFixture(TestHttpServer server) {
- Url = $"http://localhost:{server.Port}";
- _server = server;
- }
-
- public string Url { get; }
-
- readonly TestHttpServer _server;
-
- public void Dispose() => _server.Dispose();
-}
\ No newline at end of file
diff --git a/test/RestSharp.Tests.Shared/Fixtures/RequestBodyCapturer.cs b/test/RestSharp.Tests.Shared/Fixtures/RequestBodyCapturer.cs
index bb21fe4ae..bcb57764a 100644
--- a/test/RestSharp.Tests.Shared/Fixtures/RequestBodyCapturer.cs
+++ b/test/RestSharp.Tests.Shared/Fixtures/RequestBodyCapturer.cs
@@ -1,23 +1,29 @@
-using System.Net;
-using RestSharp.Tests.Shared.Extensions;
-
namespace RestSharp.Tests.Shared.Fixtures;
public class RequestBodyCapturer {
- public const string Resource = "Capture";
+ public const string Resource = "/capture";
+
+ public string ContentType { get; private set; }
+ public bool HasBody { get; private set; }
+ public string Body { get; private set; }
+ public Uri Url { get; private set; }
- public static string CapturedContentType { get; set; }
- public static bool CapturedHasEntityBody { get; set; }
- public static string CapturedEntityBody { get; set; }
- public static Uri CapturedUrl { get; set; }
+ public bool CaptureBody(string content) {
+ Body = content;
+ HasBody = !string.IsNullOrWhiteSpace(content);
+ return true;
+ }
- // ReSharper disable once UnusedMember.Global
- public static void Capture(HttpListenerContext context) {
- var request = context.Request;
+ public bool CaptureHeaders(IDictionary headers) {
+ if (headers.TryGetValue("Content-Type", out var contentType)) {
+ ContentType = contentType[0];
+ }
+
+ return true;
+ }
- CapturedContentType = request.ContentType;
- CapturedHasEntityBody = request.HasEntityBody;
- CapturedEntityBody = request.InputStream.StreamToString();
- CapturedUrl = request.Url;
+ public bool CaptureUrl(string url) {
+ Url = new Uri(url);
+ return true;
}
}
\ No newline at end of file
diff --git a/test/RestSharp.Tests.Shared/Fixtures/SimpleServer.cs b/test/RestSharp.Tests.Shared/Fixtures/SimpleServer.cs
index d53863c2f..6ebb81d13 100644
--- a/test/RestSharp.Tests.Shared/Fixtures/SimpleServer.cs
+++ b/test/RestSharp.Tests.Shared/Fixtures/SimpleServer.cs
@@ -8,17 +8,15 @@ public sealed class SimpleServer : IDisposable {
readonly WebServer _server;
readonly CancellationTokenSource _cts = new();
- public string Url { get; }
- public string ServerUrl { get; }
+ public string Url { get; }
SimpleServer(
int port,
Action handler = null,
AuthenticationSchemes authenticationSchemes = AuthenticationSchemes.Anonymous
) {
- Url = $"http://localhost:{port}/";
- ServerUrl = $"http://{Environment.MachineName}:{port}/";
- _server = new WebServer(Url, handler, authenticationSchemes);
+ Url = $"http://localhost:{port}/";
+ _server = new WebServer(Url, handler, authenticationSchemes);
Task.Run(() => _server.Run(_cts.Token));
}
diff --git a/test/RestSharp.Tests.Shared/Fixtures/TestHttpServer.cs b/test/RestSharp.Tests.Shared/Fixtures/TestHttpServer.cs
deleted file mode 100644
index b3c49b16e..000000000
--- a/test/RestSharp.Tests.Shared/Fixtures/TestHttpServer.cs
+++ /dev/null
@@ -1,112 +0,0 @@
-using System.Net;
-using System.Net.Sockets;
-using System.Text;
-
-namespace RestSharp.Tests.Shared.Fixtures;
-
-public class TestHttpServer : IDisposable {
- readonly HttpListener _listener;
- readonly List _requestHandlers;
- readonly object _requestHandlersLock = new();
- readonly CancellationTokenSource _cts = new();
-
- public int Port { get; }
-
- public TestHttpServer(
- int port,
- string url,
- Action> handlerAction,
- string hostName = "localhost"
- )
- : this(port, new List { new(url, handlerAction) }, hostName) { }
-
- public TestHttpServer(int port, List handlers, string hostName = "localhost") {
- _requestHandlers = handlers;
-
- Port = port > 0 ? port : GetRandomUnusedPort();
-
- //create and start listener
- _listener = new HttpListener();
- _listener.Prefixes.Add($"http://{hostName}:{Port}/");
- _listener.Start();
-
- Task.Run(() => HandleRequests(_cts.Token));
- }
-
- static int GetRandomUnusedPort() {
- var listener = new TcpListener(IPAddress.Any, 0);
- listener.Start();
- var port = ((IPEndPoint)listener.LocalEndpoint).Port;
- listener.Stop();
- return port;
- }
-
- async Task HandleRequests(CancellationToken cancellationToken) {
- try {
- //listen for all requests
- while (_listener.IsListening && !cancellationToken.IsCancellationRequested) {
- //get the request
- var context = await _listener.GetContextAsync();
-
- try {
- Dictionary parameters = null;
- TestRequestHandler handler;
-
- lock (_requestHandlersLock) {
- handler = _requestHandlers.FirstOrDefault(
- h => h.TryMatchUrl(context.Request.RawUrl, context.Request.HttpMethod, out parameters)
- );
- }
-
- string responseString = null;
-
- if (handler != null) {
- //add the query string parameters to the pre-defined url parameters that were set from MatchesUrl()
- foreach (var qsParamName in context.Request.QueryString.AllKeys)
- parameters[qsParamName] = context.Request.QueryString[qsParamName];
-
- try {
- handler.HandlerAction(context.Request, context.Response, parameters);
- }
- catch (Exception ex) {
- responseString = $"Exception in handler: {ex.Message}";
- context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
- }
- }
- else {
- context.Response.ContentType("text/plain").StatusCode(404);
- responseString = $"No handler provided for URL: {context.Request.RawUrl}";
- }
-
- context.Request.ClearContent();
-
- //send the response, if there is not (if responseString is null, then the handler method should have manually set the output stream)
- if (responseString != null) {
- var buffer = Encoding.UTF8.GetBytes(responseString);
- context.Response.ContentLength64 += buffer.Length;
- await context.Response.OutputStream.WriteAsync(buffer, 0, buffer.Length, cancellationToken);
- }
- }
- finally {
- context.Response.OutputStream.Close();
- context.Response.Close();
- }
- }
- }
- catch (HttpListenerException ex) {
- //when the listener is stopped, it will throw an exception for being cancelled, so just ignore it
- if (ex.Message != "The I/O operation has been aborted because of either a thread exit or an application request") throw;
- }
- }
-
- public void Dispose() {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- protected virtual void Dispose(bool disposing) {
- if (disposing && _listener.IsListening) {
- _listener.Stop();
- }
- }
-}
diff --git a/test/RestSharp.Tests.Shared/Fixtures/TestHttpServerExtensions.cs b/test/RestSharp.Tests.Shared/Fixtures/TestHttpServerExtensions.cs
deleted file mode 100644
index b1202c772..000000000
--- a/test/RestSharp.Tests.Shared/Fixtures/TestHttpServerExtensions.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Net;
-
-namespace RestSharp.Tests.Shared.Fixtures;
-
-public static class TestHttpServerExtensions {
- static readonly Dictionary RequestContent = new();
-
- internal static void ClearContent(this HttpListenerRequest request) => RequestContent.Remove(request);
-
- public static HttpListenerResponse ContentType(this HttpListenerResponse response, string contentType) {
- response.ContentType = contentType;
- return response;
- }
-
- public static HttpListenerResponse StatusCode(this HttpListenerResponse response, int statusCode) {
- response.StatusCode = statusCode;
- return response;
- }
-}
\ No newline at end of file
diff --git a/test/RestSharp.Tests.Shared/Fixtures/TestRequestHandler.cs b/test/RestSharp.Tests.Shared/Fixtures/TestRequestHandler.cs
deleted file mode 100644
index e83a8ba01..000000000
--- a/test/RestSharp.Tests.Shared/Fixtures/TestRequestHandler.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-using System.Net;
-using System.Text.RegularExpressions;
-
-namespace RestSharp.Tests.Shared.Fixtures;
-
-public class TestRequestHandler {
- readonly Regex _comparisonRegex;
-
- readonly List _urlParameterNames = new();
-
- public TestRequestHandler(
- string url,
- string httpMethod,
- Action> handlerAction
- ) {
- Url = url;
- HttpMethod = httpMethod;
- HandlerAction = handlerAction;
-
- _comparisonRegex = CreateComparisonRegex(url);
- }
-
- public TestRequestHandler(string url, Action> handlerAction)
- : this(url, null, handlerAction) { }
-
- string Url { get; }
- string HttpMethod { get; }
- internal Action> HandlerAction { get; }
-
- Regex CreateComparisonRegex(string url) {
- var regexString = Regex.Escape(url).Replace(@"\{", "{");
-
- regexString += regexString.EndsWith("/") ? "?" : "/?";
- regexString = (regexString.StartsWith("/") ? "^" : "^/") + regexString;
-
- var regex = new Regex(@"{(.*?)}");
-
- foreach (Match match in regex.Matches(regexString)) {
- regexString = regexString.Replace(match.Value, @"(.*?)");
- _urlParameterNames.Add(match.Groups[1].Value);
- }
-
- regexString += !regexString.Contains(@"\?") ? @"(\?.*)?$" : "$";
-
- return new Regex(regexString);
- }
-
- public bool TryMatchUrl(string rawUrl, string httpMethod, out Dictionary parameters) {
- var match = _comparisonRegex.Match(rawUrl);
-
- var isMethodMatched = HttpMethod == null || HttpMethod.Split(',').Contains(httpMethod);
-
- if (!match.Success || !isMethodMatched) {
- parameters = null;
- return false;
- }
-
- parameters = new Dictionary();
-
- for (var i = 0; i < _urlParameterNames.Count; i++)
- parameters[_urlParameterNames[i]] = match.Groups[i + 1].Value;
- return true;
- }
-}
\ No newline at end of file
diff --git a/test/RestSharp.Tests.Shared/Fixtures/WireMockExtensions.cs b/test/RestSharp.Tests.Shared/Fixtures/WireMockExtensions.cs
new file mode 100644
index 000000000..2cabf015c
--- /dev/null
+++ b/test/RestSharp.Tests.Shared/Fixtures/WireMockExtensions.cs
@@ -0,0 +1,21 @@
+using WireMock.RequestBuilders;
+using WireMock.ResponseBuilders;
+using WireMock.Server;
+
+namespace RestSharp.Tests.Shared.Fixtures;
+
+public static class WireMockExtensions {
+ public static RequestBodyCapturer ConfigureBodyCapturer(this WireMockServer server, Method method, bool usePath = true) {
+ var capturer = new RequestBodyCapturer();
+
+ var requestBuilder = Request
+ .Create()
+ .WithPath(usePath ? RequestBodyCapturer.Resource : "/")
+ .WithUrl(capturer.CaptureUrl)
+ .WithBody(capturer.CaptureBody)
+ .WithHeader(capturer.CaptureHeaders)
+ .UsingMethod(method.ToString().ToUpper());
+ server.Given(requestBuilder).RespondWith(Response.Create().WithStatusCode(200));
+ return capturer;
+ }
+}
\ No newline at end of file
diff --git a/test/RestSharp.Tests.Shared/RestSharp.Tests.Shared.csproj b/test/RestSharp.Tests.Shared/RestSharp.Tests.Shared.csproj
index 274f12fb9..06f32e8e6 100644
--- a/test/RestSharp.Tests.Shared/RestSharp.Tests.Shared.csproj
+++ b/test/RestSharp.Tests.Shared/RestSharp.Tests.Shared.csproj
@@ -2,4 +2,10 @@
false
+
+
+
+
+
+
diff --git a/test/RestSharp.Tests.Integrated/OAuth1Tests.cs b/test/RestSharp.Tests/OAuth1Tests.cs
similarity index 98%
rename from test/RestSharp.Tests.Integrated/OAuth1Tests.cs
rename to test/RestSharp.Tests/OAuth1Tests.cs
index 04827b1f8..d8fad50d6 100644
--- a/test/RestSharp.Tests.Integrated/OAuth1Tests.cs
+++ b/test/RestSharp.Tests/OAuth1Tests.cs
@@ -2,9 +2,10 @@
using RestSharp.Authenticators;
using RestSharp.Authenticators.OAuth;
using RestSharp.Tests.Shared.Extensions;
+
#pragma warning disable CS8618
-namespace RestSharp.Tests.Integrated;
+namespace RestSharp.Tests;
public class OAuth1Tests {
[XmlRoot("queue")]
diff --git a/test/RestSharp.Tests/RestSharp.Tests.csproj b/test/RestSharp.Tests/RestSharp.Tests.csproj
index eda460e5b..6c8d9fd1c 100644
--- a/test/RestSharp.Tests/RestSharp.Tests.csproj
+++ b/test/RestSharp.Tests/RestSharp.Tests.csproj
@@ -1,30 +1,31 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file