diff --git a/README.md b/README.md
index e41be1791..beafec739 100644
--- a/README.md
+++ b/README.md
@@ -16,26 +16,14 @@ What RestSharp adds to `HttpClient`:
Finally, RestSharp has moved to `HttpClient`. We also deprecated the following:
- SimpleJson in favour of `System.Text.Json.JsonSerialzer`
-- `IRestClient`, `IRestRequest`, and `IRestResponse` in favour of implementing classes
+- `IRestRequest`, and `IRestResponse` in favour of implementing classes
- Everything `Http` and `IHttp` as those are just wrappers
Most of the client and some of the request options are now in `RestClientOptions`.
Check [v107+ docs](https://restsharp.dev/v107) for more information.
-| :boom: Interfaces rage! |
-|:---------------------------|
-| Before you start to rage in public about interfaces that are useful for unit-testing HTTP calls,
please read [this page](https://restsharp.dev/v107/#mocking). |
-
-## Builds and Packages
-
-### Build
-
-| | |
-|-|-|
-| dev | [](https://github.com/restsharp/RestSharp/actions?query=workflow%3A%22Build+and+deploy%22) |
-
-### Nuget
+## Packages
| | |
|-|-|
@@ -48,6 +36,8 @@ Check [v107+ docs](https://restsharp.dev/v107) for more information.
This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community.
For more information see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct).
+**Users violated the aforementioned code of conduct will be blocked.**
+
## Support
RestSharp is an open-source project with a single maintainer. Do not expect your issue to be resolved unless it concerns a large group of RestSharp users.
diff --git a/RestSharp.sln.DotSettings b/RestSharp.sln.DotSettings
index 90b6b182a..764b7bbf2 100644
--- a/RestSharp.sln.DotSettings
+++ b/RestSharp.sln.DotSettings
@@ -1,4 +1,5 @@
+ False
SUGGESTION
SUGGESTION
SUGGESTION
diff --git a/benchmarks/RestSharp.Benchmarks/RestSharp.Benchmarks.csproj b/benchmarks/RestSharp.Benchmarks/RestSharp.Benchmarks.csproj
index b668e95f4..c569e156e 100644
--- a/benchmarks/RestSharp.Benchmarks/RestSharp.Benchmarks.csproj
+++ b/benchmarks/RestSharp.Benchmarks/RestSharp.Benchmarks.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/docs/serialization.md b/docs/serialization.md
index f81d7d30b..a9458523d 100644
--- a/docs/serialization.md
+++ b/docs/serialization.md
@@ -7,6 +7,15 @@ The default behavior of RestSharp is to swallow deserialization errors and retur
property of the response. Read more about it in the [Error Handling](error-handling.md).
:::
+You can tell RestSharp to use a custom serializer by using the `configureSerialization` constructor parameter:
+
+```csharp
+var client = new RestClient(
+ options,
+ configureSerialization: s => s.UseSerializer(() => new CustomSerializer());
+);
+```
+
## JSON
The default JSON serializer uses `System.Text.Json`, which is a part of .NET since .NET 6. For earlier versions, it is added as a dependency. There are also a few serializers provided as additional packages.
@@ -14,7 +23,10 @@ The default JSON serializer uses `System.Text.Json`, which is a part of .NET sin
By default, RestSharp will use `JsonSerializerDefaults.Web` configuration. If necessary, you can specify your own options:
```csharp
-client.UseSystemTextJson(new JsonSerializerOptions {...});
+var client = new RestClient(
+ options,
+ configureSerialization: s => s.UseSystemTextJson(new JsonSerializerOptions {...})
+);
```
## XML
@@ -43,7 +55,10 @@ Please note that `RestSharp.Newtonsoft.Json` package is not provided by RestShar
Use the extension method provided by the package to configure the client:
```csharp
-client.UseNewtonsoftJson();
+var client = new RestClient(
+ options,
+ configureSerialization: s => s.UseNewtonsoftJson()
+);
```
The serializer configures some options by default:
diff --git a/docs/usage.md b/docs/usage.md
index e6878f2ef..5b76b15e0 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -150,6 +150,30 @@ services.AddSingleton(
);
```
+### Simple factory
+
+Another way to create the client instance is to use a simple client factory. The factory will use the `BaseUrl` property of the client options to cache `HttpClient` instances. Every distinct base URL will get its own `HttpClient` instance. Other options don't affect the caching. Therefore, if you use different options for the same base URL, you'll get the same `HttpClient` instance, which will not be configured with the new options. Options that aren't applied _after_ the first client instance is created are:
+
+* `Credentials`
+* `UseDefaultCredentials`
+* `AutomaticDecompression`
+* `PreAuthenticate`
+* `FollowRedirects`
+* `RemoteCertificateValidationCallback`
+* `ClientCertificates`
+* `MaxRedirects`
+* `MaxTimeout`
+* `UserAgent`
+* `Expect100Continue`
+
+Constructor parameters to configure the `HttpMessageHandler` and default `HttpClient` headers configuration are also ignored for the cached instance as the factory only configures the handler once.
+
+You need to set the `useClientFactory` parameter to `true` in the `RestClient` constructor to enable the factory.
+
+```csharp
+var client = new RestClient("https://api.twitter.com/2", true);
+```
+
## Create a request
Before making a request using `RestClient`, you need to create a request instance:
@@ -166,9 +190,23 @@ var request = new RestRequest(resource, Method.Post);
After you've created a `RestRequest`, you can add parameters to it. Below, you can find all the parameter types supported by RestSharp.
-### Http Header
+### Headers
+
+Adds the header parameter as an HTTP header that is sent along with the request. The header name is the parameter's name and the header value is the value.
+
+You can use one of the following request methods to add a header parameter:
+
+```csharp
+AddHeader(string name, string value);
+AddHeader(string name, T value); // value will be converted to string
+AddOrUpdateHeader(string name, string value); // replaces the header if it already exists
+```
+
+You can also add header parameters to the client, and they will be added to every request made by the client. This is useful for adding authentication headers, for example.
-Adds the parameter as an HTTP header that is sent along with the request. The header name is the parameter's name and the header value is the value.
+```csharp
+client.AddDefaultHeader(string name, string value);
+```
::: warning Content-Type
RestSharp will use the correct content type by default. Avoid adding the `Content-Type` header manually to your requests unless you are absolutely sure it is required. You can add a custom content type to the [body parameter](#request-body) itself.
@@ -191,6 +229,14 @@ Content-Disposition: form-data; name="parameterName"
ParameterValue
```
+You can also add `GetOrPost` parameter as a default parameter to the client. This will add the parameter to every request made by the client.
+
+```csharp
+client.AddDefaultParameter("foo", "bar");
+```
+
+It will work the same way as request parameters, except that it will be added to every request.
+
#### AddObject
You can avoid calling `AddParameter` multiple times if you collect all the parameters in an object, and then use `AddObject`.
@@ -241,6 +287,26 @@ var request = new RestRequest("health/{entity}/status")
When the request executes, RestSharp will try to match any `{placeholder}` with a parameter of that name (without the `{}`) and replace it with the value. So the above code results in `health/s2/status` being the url.
+You can also add `UrlSegment` parameter as a default parameter to the client. This will add the parameter to every request made by the client.
+
+```csharp
+client.AddDefaultUrlSegment("foo", "bar");
+```
+
+### Cookies
+
+You can add cookies to a request using the `AddCookie` method:
+
+```csharp
+request.AddCookie("foo", "bar");
+```
+
+RestSharp will add cookies from the request as cookie headers and then extract the matching cookies from the response. You can observe and extract response cookies using the `RestResponse.Cookies` properties, which has the `CookieCollection` type.
+
+However, the usage of a default URL segment parameter is questionable as you can just include the parameter value to the base URL of the client. There is, however, a `CookieContainer` instance on the request level. You can either assign the pre-populated container to `request.CookieContainer`, or let the container be created by the request when you call `AddCookie`. Still, the container is only used to extract all the cookies from it and create cookie headers for the request instead of using the container directly. It's because the cookie container is normally configured on the `HttpClientHandler` level and cookies are shared between requests made by the same client. In most of the cases this behaviour can be harmful.
+
+If your use case requires sharing cookies between requests made by the client instance, you can use the client-level `CookieContainer`, which you must provide as the options' property. You can add cookies to the container using the container API. No response cookies, however, would be auto-added to the container, but you can do it in code by getting cookies from the `Cookes` property of the response and adding them to the client-level container available via `IRestClient.Options.CookieContainer` property.
+
### Request Body
RestSharp supports multiple ways to add a request body:
@@ -252,6 +318,8 @@ We recommend using `AddJsonBody` or `AddXmlBody` methods instead of `AddParamete
When you make a `POST`, `PUT` or `PATCH` request and added `GetOrPost` [parameters](#get-or-post), RestSharp will send them as a URL-encoded form request body by default. When a request also has files, it will send a `multipart/form-data` request. You can also instruct RestSharp to send the body as `multipart/form-data` by setting the `AlwaysMultipartFormData` property to `true`.
+It is not possible to add client-level default body parameters.
+
#### AddStringBody
If you have a pre-serialized payload like a JSON string, you can use `AddStringBody` to add it as a body parameter. You need to specify the content type, so the remote endpoint knows what to do with the request body. For example:
@@ -322,6 +390,14 @@ To do so, set the `encode` argument to `false` when adding the parameter:
request.AddQueryParameter("foo", "bar/fox", false);
```
+You can also add a query string parameter as a default parameter to the client. This will add the parameter to every request made by the client.
+
+```csharp
+client.AddDefaultQueryParameter("foo", "bar");
+```
+
+The line above will result in all the requests made by that client instance to have `foo=bar` in the query string for all the requests made by that client.
+
## Making a call
Once you've added all the parameters to your `RestRequest`, you are ready to make a request.
@@ -354,7 +430,7 @@ Task> ExecutePostAsync(RestRequest request, CancellationToken
Task> ExecutePutAsync(RestRequest request, CancellationToken cancellationToken)
```
-All the overloads that return `RestResponse` or `RestResponse` don't throw an exception if the server returns an error. Read more about it [here](error-handling.md).
+All the overloads with names starting with `Execute` don't throw an exception if the server returns an error. Read more about it [here](error-handling.md).
If you just need a deserialized response, you can use one of the extensions:
@@ -369,6 +445,17 @@ Task DeleteAsync(RestRequest request, CancellationToken cancellationToken)
Those extensions will throw an exception if the server returns an error, as there's no other way to float the error back to the caller.
+The `IRestClient` interface also has extensions for making requests without deserialization, which throw an exception if the server returns an error even if the client is configured to not throw exceptions.
+
+```csharp
+Task GetAsync(RestRequest request, CancellationToken cancellationToken)
+Task PostAsync(RestRequest request, CancellationToken cancellationToken)
+Task PutAsync(RestRequest request, CancellationToken cancellationToken)
+Task HeadAsync(RestRequest request, CancellationToken cancellationToken)
+Task PatchAsync(RestRequest request, CancellationToken cancellationToken)
+Task DeleteAsync(RestRequest request, CancellationToken cancellationToken)
+```
+
### JSON requests
To make a simple `GET` call and get a deserialized JSON response with a pre-formed resource string, use this:
@@ -474,15 +561,7 @@ One way of doing it is to use `RestClient` constructors that accept an instance
- `UserAgent` will be set if the `User-Agent` header is not set on the `HttpClient` instance already.
- `Expect100Continue`
-Another option is to use a simple HTTP client factory. It is a static factory, which holds previously instantiated `HttpClient` instances. It can be used to create `RestClient` instances that share the same `HttpClient` instance. The cache key is the `BaseUrl` provided in the options. When you opt-in to use the factory and don't set `BaseUrl`, the `RestClient` constructor will crash.
-
-```csharp
-var client = new RestClient(new Uri("https://example.org/api"), useClientFactory: true);
-```
-
-::: warning
-Note that the `RestClient` constructor will not reconfigure the `HttpClient` instance if it's already in the cache. Therefore, you should not try using the factory when providing different options for the same base URL.
-:::
+Another option is to use a simple HTTP client factory as described [above](#simple-factory).
## Blazor support
diff --git a/docs/v107/README.md b/docs/v107/README.md
index 8088c5ae8..97305c749 100644
--- a/docs/v107/README.md
+++ b/docs/v107/README.md
@@ -224,12 +224,12 @@ Below, you can find members of `IRestClient` and `IRestRequest` with their corre
| `IRestClient` member | Where is it now? |
|:------------------------------------------------------------------------------------------------|:-----------------------------------|
-| `CookieContainer` | `RestClient` |
+| `CookieContainer` | `RestClientOptions` |
| `AutomaticDecompression` | `RestClientOptions`, changed type |
| `MaxRedirects` | `RestClientOptions` |
| `UserAgent` | `RestClientOptions` |
| `Timeout` | `RestClientOptions`, `RestRequest` |
-| `Authenticator` | `RestClient` |
+| `Authenticator` | `RestClientOptions` |
| `BaseUrl` | `RestClientOptions` |
| `Encoding` | `RestClientOptions` |
| `ThrowOnDeserializationError` | `RestClientOptions` |
@@ -249,12 +249,8 @@ Below, you can find members of `IRestClient` and `IRestRequest` with their corre
| `ReadWriteTimeout` | Not supported |
| `UseSynchronizationContext` | Not supported |
| `DefaultParameters` | `RestClient` |
-| `UseSerializer(Func serializerFactory)` | `RestClient` |
-| `UseSerializer()` | `RestClient` |
-| `Deserialize(IRestResponse response)` | `RestClient` |
-| `BuildUri(IRestRequest request)` | `RestClient` |
-| `UseUrlEncoder(Func encoder)` | Extension |
-| `UseQueryEncoder(Func queryEncoder)` | Extension |
+| `Deserialize(IRestResponse response)` | `RestSerializers` |
+| `BuildUri(IRestRequest request)` | Extension |
| `ExecuteAsync(IRestRequest request, CancellationToken cancellationToken)` | `RestClient` |
| `ExecuteAsync(IRestRequest request, Method httpMethod, CancellationToken cancellationToken)` | Extension |
| `ExecuteAsync(IRestRequest request, Method httpMethod, CancellationToken cancellationToken)` | Extension |
@@ -272,7 +268,7 @@ Below, you can find members of `IRestClient` and `IRestRequest` with their corre
| `ExecuteAsPost(IRestRequest request, string httpMethod)` | Deprecated |
| `ExecuteAsGet(IRestRequest request, string httpMethod)` | Deprecated |
| `ExecuteAsPost(IRestRequest request, string httpMethod)` | Deprecated |
-| `BuildUriWithoutQueryParameters(IRestRequest request)` | Removed |
+| `BuildUriWithoutQueryParameters(IRestRequest request)` | Extension |
| `ConfigureWebRequest(Action configurator)` | Removed |
| `AddHandler(string contentType, Func deserializerFactory)` | Removed |
| `RemoveHandler(string contentType)` | Removed |
diff --git a/gen/SourceGenerator/SourceGenerator.csproj b/gen/SourceGenerator/SourceGenerator.csproj
index 6b640d2f8..9e63fcfeb 100644
--- a/gen/SourceGenerator/SourceGenerator.csproj
+++ b/gen/SourceGenerator/SourceGenerator.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index e38e2e8eb..13dd1b1c0 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -20,7 +20,7 @@
-
+
diff --git a/src/RestSharp/Authenticators/AuthenticatorBase.cs b/src/RestSharp/Authenticators/AuthenticatorBase.cs
index c6507dc35..1f311dba4 100644
--- a/src/RestSharp/Authenticators/AuthenticatorBase.cs
+++ b/src/RestSharp/Authenticators/AuthenticatorBase.cs
@@ -21,6 +21,6 @@ public abstract class AuthenticatorBase : IAuthenticator {
protected abstract ValueTask GetAuthenticationParameter(string accessToken);
- public async ValueTask Authenticate(RestClient client, RestRequest request)
+ public async ValueTask Authenticate(IRestClient client, RestRequest request)
=> request.AddOrUpdateParameter(await GetAuthenticationParameter(Token).ConfigureAwait(false));
}
\ No newline at end of file
diff --git a/src/RestSharp/Authenticators/IAuthenticator.cs b/src/RestSharp/Authenticators/IAuthenticator.cs
index 87638a13f..b7cbe9606 100644
--- a/src/RestSharp/Authenticators/IAuthenticator.cs
+++ b/src/RestSharp/Authenticators/IAuthenticator.cs
@@ -15,5 +15,5 @@
namespace RestSharp.Authenticators;
public interface IAuthenticator {
- ValueTask Authenticate(RestClient client, RestRequest request);
+ ValueTask Authenticate(IRestClient client, RestRequest request);
}
\ No newline at end of file
diff --git a/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs b/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs
index 05e515f72..93e0c6bff 100644
--- a/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs
+++ b/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs
@@ -16,6 +16,7 @@
using RestSharp.Extensions;
using System.Web;
+// ReSharper disable NotResolvedInText
// ReSharper disable CheckNamespace
namespace RestSharp.Authenticators;
@@ -38,7 +39,7 @@ public class OAuth1Authenticator : IAuthenticator {
public virtual string? ClientUsername { get; set; }
public virtual string? ClientPassword { get; set; }
- public ValueTask Authenticate(RestClient client, RestRequest request) {
+ public ValueTask Authenticate(IRestClient client, RestRequest request) {
var workflow = new OAuthWorkflow {
ConsumerKey = ConsumerKey,
ConsumerSecret = ConsumerSecret,
@@ -64,8 +65,8 @@ public static OAuth1Authenticator ForRequestToken(
string consumerKey,
string? consumerSecret,
OAuthSignatureMethod signatureMethod = OAuthSignatureMethod.HmacSha1
- ) {
- var authenticator = new OAuth1Authenticator {
+ )
+ => new() {
ParameterHandling = OAuthParameterHandling.HttpAuthorizationHeader,
SignatureMethod = signatureMethod,
SignatureTreatment = OAuthSignatureTreatment.Escaped,
@@ -74,10 +75,6 @@ public static OAuth1Authenticator ForRequestToken(
Type = OAuthType.RequestToken
};
- return authenticator;
- }
-
- [PublicAPI]
public static OAuth1Authenticator ForRequestToken(string consumerKey, string? consumerSecret, string callbackUrl) {
var authenticator = ForRequestToken(consumerKey, consumerSecret);
@@ -105,7 +102,6 @@ public static OAuth1Authenticator ForAccessToken(
Type = OAuthType.AccessToken
};
- [PublicAPI]
public static OAuth1Authenticator ForAccessToken(
string consumerKey,
string? consumerSecret,
@@ -171,7 +167,6 @@ public static OAuth1Authenticator ForClientAuthentication(
Type = OAuthType.ClientAuthentication
};
- [PublicAPI]
public static OAuth1Authenticator ForProtectedResource(
string consumerKey,
string? consumerSecret,
@@ -190,7 +185,7 @@ public static OAuth1Authenticator ForProtectedResource(
TokenSecret = accessTokenSecret
};
- void AddOAuthData(RestClient client, RestRequest request, OAuthWorkflow workflow) {
+ void AddOAuthData(IRestClient client, RestRequest request, OAuthWorkflow workflow) {
var requestUrl = client.BuildUriWithoutQueryParameters(request).AbsoluteUri;
if (requestUrl.Contains('?'))
@@ -201,11 +196,9 @@ void AddOAuthData(RestClient client, RestRequest request, OAuthWorkflow workflow
var url = client.BuildUri(request).ToString();
var queryStringStart = url.IndexOf('?');
- if (queryStringStart != -1)
- url = url.Substring(0, queryStringStart);
-
- var method = request.Method.ToString().ToUpperInvariant();
+ if (queryStringStart != -1) url = url.Substring(0, queryStringStart);
+ var method = request.Method.ToString().ToUpperInvariant();
var parameters = new WebPairCollection();
// include all GET and POST parameters before generating the signature
@@ -247,21 +240,18 @@ void AddOAuthData(RestClient client, RestRequest request, OAuthWorkflow workflow
request.AddOrUpdateParameters(oauthParameters);
- IEnumerable CreateHeaderParameters()
- => new[] { new HeaderParameter(KnownHeaders.Authorization, GetAuthorizationHeader()) };
+ IEnumerable CreateHeaderParameters() => new[] { new HeaderParameter(KnownHeaders.Authorization, GetAuthorizationHeader()) };
- IEnumerable CreateUrlParameters()
- => oauth.Parameters.Select(p => new GetOrPostParameter(p.Name, HttpUtility.UrlDecode(p.Value)));
+ IEnumerable CreateUrlParameters() => oauth.Parameters.Select(p => new GetOrPostParameter(p.Name, HttpUtility.UrlDecode(p.Value)));
string GetAuthorizationHeader() {
var oathParameters =
oauth.Parameters
.OrderBy(x => x, WebPair.Comparer)
- .Select(x => $"{x.Name}=\"{x.WebValue}\"")
+ .Select(x => x.GetQueryParameter(true))
.ToList();
- if (!Realm.IsEmpty())
- oathParameters.Insert(0, $"realm=\"{OAuthTools.UrlEncodeRelaxed(Realm!)}\"");
+ if (!Realm.IsEmpty()) oathParameters.Insert(0, $"realm=\"{OAuthTools.UrlEncodeRelaxed(Realm)}\"");
return $"OAuth {string.Join(",", oathParameters)}";
}
@@ -270,5 +260,5 @@ string GetAuthorizationHeader() {
static class ParametersExtensions {
internal static IEnumerable ToWebParameters(this IEnumerable p)
- => p.Select(x => new WebPair(Ensure.NotNull(x.Name, "Parameter name"), Ensure.NotNull(x.Value, "Parameter value").ToString()!));
-}
\ No newline at end of file
+ => p.Select(x => new WebPair(Ensure.NotNull(x.Name, "Parameter name"), x.Value?.ToString()));
+}
diff --git a/src/RestSharp/Authenticators/OAuth/OAuthTools.cs b/src/RestSharp/Authenticators/OAuth/OAuthTools.cs
index f9d126af7..3badb1692 100644
--- a/src/RestSharp/Authenticators/OAuth/OAuthTools.cs
+++ b/src/RestSharp/Authenticators/OAuth/OAuthTools.cs
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
using System.Text;
using RestSharp.Authenticators.OAuth.Extensions;
@@ -88,7 +89,10 @@ public static string GetNonce() {
/// actually worked (which in my experiments it doesn't), we can't rely on every
/// host actually having this configuration element present.
///
- public static string UrlEncodeRelaxed(string value) {
+ [return: NotNullIfNotNull(nameof(value))]
+ public static string? UrlEncodeRelaxed(string? value) {
+ if (value == null) return null;
+
// Escape RFC 3986 chars first.
var escapedRfc3986 = new StringBuilder(value);
@@ -117,8 +121,9 @@ public static string UrlEncodeRelaxed(string value) {
// Generic Syntax," .) section 2.3) MUST be encoded.
// ...
// unreserved = ALPHA, DIGIT, '-', '.', '_', '~'
- public static string UrlEncodeStrict(string value)
- => string.Join("", value.Select(x => Unreserved.Contains(x) ? x.ToString() : $"%{(byte)x:X2}"));
+ [return: NotNullIfNotNull(nameof(value))]
+ public static string? UrlEncodeStrict(string? value)
+ => value == null ? null : string.Join("", value.Select(x => Unreserved.Contains(x) ? x.ToString() : $"%{(byte)x:X2}"));
///
/// Sorts a collection of key-value pairs by name, and then value if equal,
@@ -137,9 +142,9 @@ public static string UrlEncodeStrict(string value)
public static IEnumerable SortParametersExcludingSignature(WebPairCollection parameters)
=> parameters
.Where(x => !x.Name.EqualsIgnoreCase("oauth_signature"))
- .Select(x => new WebPair(UrlEncodeStrict(x.Name), UrlEncodeStrict(x.Value), x.Encode))
+ .Select(x => new WebPair(UrlEncodeStrict(x.Name), UrlEncodeStrict(x.Value)))
.OrderBy(x => x, WebPair.Comparer)
- .Select(x => $"{x.Name}={x.Value}");
+ .Select(x => x.GetQueryParameter(false));
///
/// Creates a request URL suitable for making OAuth requests.
@@ -151,8 +156,8 @@ public static IEnumerable SortParametersExcludingSignature(WebPairCollec
static string ConstructRequestUrl(Uri url) {
Ensure.NotNull(url, nameof(url));
- var basic = url.Scheme == "http" && url.Port == 80;
- var secure = url.Scheme == "https" && url.Port == 443;
+ var basic = url is { Scheme: "http", Port : 80 };
+ var secure = url is { Scheme: "https", Port: 443 };
var port = basic || secure ? "" : $":{url.Port}";
return $"{url.Scheme}://{url.Host}{port}{url.AbsolutePath}";
diff --git a/src/RestSharp/Authenticators/OAuth/WebPair.cs b/src/RestSharp/Authenticators/OAuth/WebPair.cs
index fc4f3e604..d83572e05 100644
--- a/src/RestSharp/Authenticators/OAuth/WebPair.cs
+++ b/src/RestSharp/Authenticators/OAuth/WebPair.cs
@@ -15,17 +15,20 @@
namespace RestSharp.Authenticators.OAuth;
class WebPair {
- public WebPair(string name, string value, bool encode = false) {
+ public WebPair(string name, string? value, bool encode = false) {
Name = name;
Value = value;
WebValue = encode ? OAuthTools.UrlEncodeRelaxed(value) : value;
- Encode = encode;
}
- public string Name { get; }
- public string Value { get; }
- public string WebValue { get; }
- public bool Encode { get; }
+ public string Name { get; }
+ public string? Value { get; }
+ string? WebValue { get; }
+
+ public string GetQueryParameter(bool web) {
+ var value = web ? $"\"{WebValue}\"" : Value;
+ return value == null ? Name : $"{Name}={value}";
+ }
internal static WebPairComparer Comparer { get; } = new();
@@ -36,4 +39,4 @@ public int Compare(WebPair? x, WebPair? y) {
return compareName != 0 ? compareName : string.CompareOrdinal(x?.Value, y?.Value);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/RestSharp/BuildUriExtensions.cs b/src/RestSharp/BuildUriExtensions.cs
new file mode 100644
index 000000000..13c663b81
--- /dev/null
+++ b/src/RestSharp/BuildUriExtensions.cs
@@ -0,0 +1,93 @@
+// 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;
+
+public static class BuildUriExtensions {
+ ///
+ /// Builds the URI for the request
+ ///
+ /// Client instance
+ /// Request instance
+ ///
+ public static Uri BuildUri(this IRestClient client, RestRequest request) {
+ DoBuildUriValidations(client, request);
+
+ var (uri, resource) = client.Options.BaseUrl.GetUrlSegmentParamsValues(
+ request.Resource,
+ client.Options.Encode,
+ request.Parameters,
+ client.DefaultParameters
+ );
+ var mergedUri = uri.MergeBaseUrlAndResource(resource);
+ var query = client.GetRequestQuery(request);
+ return mergedUri.AddQueryString(query);
+ }
+
+ ///
+ /// Builds the URI for the request without query parameters.
+ ///
+ /// Client instance
+ /// Request instance
+ ///
+ public static Uri BuildUriWithoutQueryParameters(this IRestClient client, RestRequest request) {
+ DoBuildUriValidations(client, request);
+
+ var (uri, resource) = client.Options.BaseUrl.GetUrlSegmentParamsValues(
+ request.Resource,
+ client.Options.Encode,
+ request.Parameters,
+ client.DefaultParameters
+ );
+ return uri.MergeBaseUrlAndResource(resource);
+ }
+
+ ///
+ /// Gets the query string for the request.
+ ///
+ /// Client instance
+ /// Request instance
+ ///
+ [PublicAPI]
+ public static string? GetRequestQuery(this IRestClient client, RestRequest request) {
+ var parametersCollections = new ParametersCollection[] { request.Parameters, client.DefaultParameters };
+
+ var parameters = parametersCollections.SelectMany(x => x.GetQueryParameters(request.Method)).ToList();
+
+ return parameters.Count == 0 ? null : string.Join("&", parameters.Select(EncodeParameter).ToArray());
+
+ string GetString(string name, string? value, Func? encode) {
+ var val = encode != null && value != null ? encode(value) : value;
+ return val == null ? name : $"{name}={val}";
+ }
+
+ string EncodeParameter(Parameter parameter)
+ => !parameter.Encode
+ ? GetString(parameter.Name!, parameter.Value?.ToString(), null)
+ : GetString(
+ client.Options.EncodeQuery(parameter.Name!, client.Options.Encoding),
+ parameter.Value?.ToString(),
+ x => client.Options.EncodeQuery(x, client.Options.Encoding)
+ );
+ }
+
+ static void DoBuildUriValidations(IRestClient client, RestRequest request) {
+ if (client.Options.BaseUrl == null && !request.Resource.ToLowerInvariant().StartsWith("http"))
+ throw new ArgumentOutOfRangeException(
+ nameof(request),
+ "Request resource doesn't contain a valid scheme for an empty base URL of the client"
+ );
+ }
+}
diff --git a/src/RestSharp/IRestClient.cs b/src/RestSharp/IRestClient.cs
index ab203db07..3f37b536d 100644
--- a/src/RestSharp/IRestClient.cs
+++ b/src/RestSharp/IRestClient.cs
@@ -28,6 +28,11 @@ public interface IRestClient : IDisposable {
///
RestSerializers Serializers { get; }
+ ///
+ /// Default parameters to use on every request made with this client instance.
+ ///
+ DefaultParameters DefaultParameters { get; }
+
///
/// Executes the request asynchronously, authenticating if needed
///
diff --git a/src/RestSharp/Options/RestClientOptions.cs b/src/RestSharp/Options/RestClientOptions.cs
index d5efd312e..a07daf55d 100644
--- a/src/RestSharp/Options/RestClientOptions.cs
+++ b/src/RestSharp/Options/RestClientOptions.cs
@@ -136,6 +136,12 @@ public RestClientOptions(string baseUrl) : this(new Uri(Ensure.NotEmptyString(ba
///
public string? BaseHost { get; set; }
+ ///
+ /// Custom cookie container to be used for requests. RestSharp will not assign the container to the message handler,
+ /// but will fetch cookies from it and set them on the request.
+ ///
+ public CookieContainer? CookieContainer { get; set; }
+
///
/// Maximum request duration in milliseconds. When the request timeout is specified using ,
/// the lowest value between the client timeout and request timeout will be used.
diff --git a/src/RestSharp/Parameters/DefaultParameters.cs b/src/RestSharp/Parameters/DefaultParameters.cs
new file mode 100644
index 000000000..02373f713
--- /dev/null
+++ b/src/RestSharp/Parameters/DefaultParameters.cs
@@ -0,0 +1,43 @@
+// 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;
+
+public sealed class DefaultParameters : ParametersCollection {
+ readonly ReadOnlyRestClientOptions _options;
+
+ readonly object _lock = new();
+
+ public DefaultParameters(ReadOnlyRestClientOptions options) => _options = options;
+
+ public void 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 (!_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);
+ }
+ }
+
+ static readonly ParameterType[] MultiParameterTypes = { ParameterType.QueryString, ParameterType.GetOrPost };
+}
diff --git a/src/RestSharp/Parameters/Parameter.cs b/src/RestSharp/Parameters/Parameter.cs
index f37cd9f2d..93de83479 100644
--- a/src/RestSharp/Parameters/Parameter.cs
+++ b/src/RestSharp/Parameters/Parameter.cs
@@ -27,7 +27,7 @@ public abstract record Parameter(string? Name, object? Value, ParameterType Type
/// Return a human-readable representation of this parameter
///
/// String
- public override string ToString() => $"{Name}={Value}";
+ public sealed override string ToString() => Value == null ? $"{Name}" : $"{Name}={Value}";
public static Parameter CreateParameter(string? name, object? value, ParameterType type, bool encode = true)
// ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault
diff --git a/src/RestSharp/Parameters/ParametersCollection.cs b/src/RestSharp/Parameters/ParametersCollection.cs
index 8423f7f93..2ee80c3f6 100644
--- a/src/RestSharp/Parameters/ParametersCollection.cs
+++ b/src/RestSharp/Parameters/ParametersCollection.cs
@@ -17,43 +17,25 @@
namespace RestSharp;
-public class ParametersCollection : IReadOnlyCollection {
- readonly List _parameters = new();
+public abstract class ParametersCollection : IReadOnlyCollection {
+ protected readonly List Parameters = new();
- public ParametersCollection() { }
-
- public ParametersCollection(IEnumerable parameters) => _parameters.AddRange(parameters);
-
- public ParametersCollection AddParameters(IEnumerable parameters) {
- _parameters.AddRange(parameters);
- return this;
- }
-
- public ParametersCollection AddParameters(ParametersCollection parameters) {
- _parameters.AddRange(parameters);
- return this;
- }
-
- public void AddParameter(Parameter parameter) => _parameters.Add(parameter);
-
- public void RemoveParameter(string name) => _parameters.RemoveAll(x => x.Name == name);
-
- public void RemoveParameter(Parameter parameter) => _parameters.Remove(parameter);
+ // public ParametersCollection(IEnumerable parameters) => _parameters.AddRange(parameters);
static readonly Func SearchPredicate = (p, name)
=> p.Name != null && p.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase);
- public bool Exists(Parameter parameter) => _parameters.Any(p => SearchPredicate(p, parameter.Name) && p.Type == parameter.Type);
+ public bool Exists(Parameter parameter) => Parameters.Any(p => SearchPredicate(p, parameter.Name) && p.Type == parameter.Type);
- public Parameter? TryFind(string parameterName) => _parameters.FirstOrDefault(x => SearchPredicate(x, parameterName));
+ public Parameter? TryFind(string parameterName) => Parameters.FirstOrDefault(x => SearchPredicate(x, parameterName));
- public ParametersCollection GetParameters(ParameterType parameterType) => new(_parameters.Where(x => x.Type == parameterType));
+ public IEnumerable GetParameters(ParameterType parameterType) => Parameters.Where(x => x.Type == parameterType);
- public IEnumerable GetParameters() where T : class => _parameters.OfType();
+ public IEnumerable GetParameters() where T : class => Parameters.OfType();
- public IEnumerator GetEnumerator() => _parameters.GetEnumerator();
+ public IEnumerator GetEnumerator() => Parameters.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
- public int Count => _parameters.Count;
+ public int Count => Parameters.Count;
}
\ No newline at end of file
diff --git a/src/RestSharp/Parameters/RequestParameters.cs b/src/RestSharp/Parameters/RequestParameters.cs
new file mode 100644
index 000000000..4545488b3
--- /dev/null
+++ b/src/RestSharp/Parameters/RequestParameters.cs
@@ -0,0 +1,38 @@
+// 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;
+
+public sealed class RequestParameters : ParametersCollection {
+ public RequestParameters() { }
+
+ public RequestParameters(IEnumerable parameters) => Parameters.AddRange(parameters);
+
+ public ParametersCollection AddParameters(IEnumerable parameters) {
+ Parameters.AddRange(parameters);
+ return this;
+ }
+
+ public ParametersCollection AddParameters(ParametersCollection parameters) {
+ Parameters.AddRange(parameters);
+ return this;
+ }
+
+ public void AddParameter(Parameter parameter) => Parameters.Add(parameter);
+
+ public void RemoveParameter(string name) => Parameters.RemoveAll(x => x.Name == name);
+
+ public void RemoveParameter(Parameter parameter) => Parameters.Remove(parameter);
+}
diff --git a/src/RestSharp/Request/RequestContent.cs b/src/RestSharp/Request/RequestContent.cs
index bf900be82..308d1ba86 100644
--- a/src/RestSharp/Request/RequestContent.cs
+++ b/src/RestSharp/Request/RequestContent.cs
@@ -23,20 +23,22 @@
namespace RestSharp;
class RequestContent : IDisposable {
- readonly RestClient _client;
- readonly RestRequest _request;
- readonly List _streams = new();
+ readonly RestClient _client;
+ readonly RestRequest _request;
+ readonly List _streams = new();
+ readonly ParametersCollection _parameters;
HttpContent? Content { get; set; }
public RequestContent(RestClient client, RestRequest request) {
- _client = client;
- _request = request;
+ _client = client;
+ _request = request;
+ _parameters = new RequestParameters(_request.Parameters.Union(_client.DefaultParameters));
}
public HttpContent BuildContent() {
AddFiles();
- var postParameters = _request.Parameters.GetContentParameters(_request.Method).ToArray();
+ var postParameters = _parameters.GetContentParameters(_request.Method).ToArray();
AddBody(postParameters.Length > 0);
AddPostParameters(postParameters);
AddHeaders();
@@ -47,7 +49,7 @@ public HttpContent BuildContent() {
void AddFiles() {
if (!_request.HasFiles() && !_request.AlwaysMultipartFormData) return;
- var mpContent = new MultipartFormDataContent(GetOrSetFormBoundary());
+ var mpContent = CreateMultipartFormDataContent();
foreach (var file in _request.Files) {
var stream = file.GetFile();
@@ -58,10 +60,7 @@ void AddFiles() {
var dispositionHeader = file.Options.DisableFilenameEncoding
? ContentDispositionHeaderValue.Parse($"form-data; name=\"{file.Name}\"; filename=\"{file.FileName}\"")
- : new ContentDispositionHeaderValue("form-data") {
- Name = $"\"{file.Name}\"",
- FileName = $"\"{file.FileName}\""
- };
+ : new ContentDispositionHeaderValue("form-data") { Name = $"\"{file.Name}\"", FileName = $"\"{file.FileName}\"" };
if (!file.Options.DisableFileNameStar) dispositionHeader.FileNameStar = file.FileName;
fileContent.Headers.ContentDisposition = dispositionHeader;
@@ -102,11 +101,7 @@ HttpContent GetSerialized() {
var contentType = body.ContentType.Or(serializer.Serializer.ContentType);
- return new StringContent(
- content,
- _client.Options.Encoding,
- contentType.Value
- );
+ return new StringContent(content, _client.Options.Encoding, contentType.Value);
}
}
@@ -117,6 +112,15 @@ static bool BodyShouldBeMultipartForm(BodyParameter bodyParameter) {
string GetOrSetFormBoundary() => _request.FormBoundary ?? (_request.FormBoundary = Guid.NewGuid().ToString());
+ MultipartFormDataContent CreateMultipartFormDataContent() {
+ var boundary = GetOrSetFormBoundary();
+ var mpContent = new MultipartFormDataContent(boundary);
+ var contentType = new MediaTypeHeaderValue("multipart/form-data");
+ contentType.Parameters.Add(new NameValueHeaderValue(nameof(boundary), GetBoundary(boundary, _request.MultipartFormQuoteBoundary)));
+ mpContent.Headers.ContentType = contentType;
+ return mpContent;
+ }
+
void AddBody(bool hasPostParameters) {
if (!_request.TryGetBodyParameter(out var bodyParameter)) return;
@@ -125,7 +129,7 @@ void AddBody(bool hasPostParameters) {
// we need to send the body
if (hasPostParameters || _request.HasFiles() || BodyShouldBeMultipartForm(bodyParameter!) || _request.AlwaysMultipartFormData) {
// here we must use multipart form data
- var mpContent = Content as MultipartFormDataContent ?? new MultipartFormDataContent(GetOrSetFormBoundary());
+ var mpContent = Content as MultipartFormDataContent ?? CreateMultipartFormDataContent();
var ct = bodyContent.Headers.ContentType?.MediaType;
var name = bodyParameter!.Name.IsEmpty() ? ct : bodyParameter.Name;
@@ -154,31 +158,27 @@ void AddPostParameters(GetOrPostParameter[] postParameters) {
var parameterName = postParameter.Name!;
mpContent.Add(
- new StringContent(postParameter.Value?.ToString() ?? "", _client.Options.Encoding, postParameter.ContentType.Value),
+ new StringContent(postParameter.Value?.ToString() ?? string.Empty, _client.Options.Encoding, postParameter.ContentType.Value),
_request.MultipartFormQuoteParameters ? $"\"{parameterName}\"" : parameterName
);
}
}
else {
-#if NET
- // We should not have anything else except the parameters, so we send them as form URL encoded.
- var formContent = new FormUrlEncodedContent(
- postParameters
- .Select(x => new KeyValuePair(x.Name!, x.Value?.ToString() ?? string.Empty))!
- );
- Content = formContent;
-#else
- // However due to bugs in HttpClient FormUrlEncodedContent (see https://github.com/restsharp/RestSharp/issues/1814) we
- // do the encoding ourselves using WebUtility.UrlEncode instead.
- var encodedItems = postParameters.Select(x => $"{x.Name!.UrlEncode()}={x.Value?.ToString()?.UrlEncode() ?? string.Empty}");
- var encodedContent = new StringContent(encodedItems.JoinToString("&"), null, ContentType.FormUrlEncoded.Value);
+ 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) {
+ encodedContent.Headers.ContentType!.CharSet = "";
+ }
+
Content = encodedContent;
-#endif
}
}
+ static string GetBoundary(string boundary, bool quote) => quote ? $"\"{boundary}\"" : boundary;
+
void AddHeaders() {
- var contentHeaders = _request.Parameters
+ var contentHeaders = _parameters
.GetParameters()
.Where(x => IsContentHeader(x.Name!))
.ToArray();
@@ -203,7 +203,7 @@ void AddHeader(HeaderParameter parameter) {
string GetContentTypeHeader(string contentType)
=> Content is MultipartFormDataContent
- ? $"{contentType}; boundary=\"{GetOrSetFormBoundary()}\""
+ ? $"{contentType}; boundary={GetBoundary(GetOrSetFormBoundary(), _request.MultipartFormQuoteBoundary)}"
: contentType;
}
diff --git a/src/RestSharp/Request/RequestHeaders.cs b/src/RestSharp/Request/RequestHeaders.cs
index cec79961e..0c159fd55 100644
--- a/src/RestSharp/Request/RequestHeaders.cs
+++ b/src/RestSharp/Request/RequestHeaders.cs
@@ -21,7 +21,7 @@
namespace RestSharp;
class RequestHeaders {
- public ParametersCollection Parameters { get; } = new();
+ public RequestParameters Parameters { get; } = new();
public RequestHeaders AddHeaders(ParametersCollection parameters) {
Parameters.AddParameters(parameters.GetParameters());
diff --git a/src/RestSharp/Request/RestRequest.cs b/src/RestSharp/Request/RestRequest.cs
index 0cc33ba16..bf70b4f92 100644
--- a/src/RestSharp/Request/RestRequest.cs
+++ b/src/RestSharp/Request/RestRequest.cs
@@ -82,19 +82,28 @@ public RestRequest(Uri resource, Method method = Method.Get)
public bool AlwaysMultipartFormData { get; set; }
///
- /// When set to true, parameters in a multipart form data requests will be enclosed in
+ /// When set to true, parameter values in a multipart form data requests will be enclosed in
/// quotation marks. Default is false. Enable it if the remote endpoint requires parameters
/// to be in quotes (for example, FreshDesk API).
///
public bool MultipartFormQuoteParameters { get; set; }
+ ///
+ /// When set to true, the form boundary part of the content type will be enclosed in
+ /// quotation marks. Default is true.
+ ///
+ public bool MultipartFormQuoteBoundary { get; set; } = true;
+
+ ///
+ /// Overrides the default (random) form boundary
+ ///
public string? FormBoundary { get; set; }
///
/// Container of all HTTP parameters to be passed with the request.
/// See AddParameter() for explanation of the types of parameters that can be passed
///
- public ParametersCollection Parameters { get; } = new();
+ public RequestParameters Parameters { get; } = new();
///
/// Optional cookie container to use for the request. If not set, cookies are not passed.
diff --git a/src/RestSharp/Request/UriExtensions.cs b/src/RestSharp/Request/UriExtensions.cs
index 818ddbda4..887e9ed7d 100644
--- a/src/RestSharp/Request/UriExtensions.cs
+++ b/src/RestSharp/Request/UriExtensions.cs
@@ -35,33 +35,13 @@ public static Uri MergeBaseUrlAndResource(this Uri? baseUrl, string? resource) {
return assembled != null ? new Uri(usingBaseUri, assembled) : baseUrl;
}
- public static Uri ApplyQueryStringParamsValuesToUri(
- this Uri mergedUri,
- Method method,
- Encoding encoding,
- Func encodeQuery,
- params ParametersCollection[] parametersCollections
- ) {
- var parameters = parametersCollections.SelectMany(x => x.GetQueryParameters(method)).ToList();
-
- if (parameters.Count == 0) return mergedUri;
-
- var uri = mergedUri.AbsoluteUri;
- var separator = uri.Contains('?') ? "&" : "?";
+ public static Uri AddQueryString(this Uri uri, string? query) {
+ if (query == null) return uri;
- return new Uri(string.Concat(uri, separator, EncodeParameters()));
-
- string EncodeParameters() => string.Join("&", parameters.Select(EncodeParameter).ToArray());
-
- string GetString(string name, string? value, Func? encode) {
- var val = encode != null && value != null ? encode(value) : value;
- return val == null ? name : $"{name}={val}";
- }
+ var absoluteUri = uri.AbsoluteUri;
+ var separator = absoluteUri.Contains('?') ? "&" : "?";
- string EncodeParameter(Parameter parameter)
- => !parameter.Encode
- ? GetString(parameter.Name!, parameter.Value?.ToString(), null)
- : GetString(encodeQuery(parameter.Name!, encoding), parameter.Value?.ToString(), x => encodeQuery(x, encoding));
+ return new Uri($"{absoluteUri}{separator}{query}");
}
public static UrlSegmentParamsValues GetUrlSegmentParamsValues(
diff --git a/src/RestSharp/RestClient.Async.cs b/src/RestSharp/RestClient.Async.cs
index 0848f8442..821f4f487 100644
--- a/src/RestSharp/RestClient.Async.cs
+++ b/src/RestSharp/RestClient.Async.cs
@@ -92,7 +92,7 @@ async Task ExecuteRequestAsync(RestRequest request, CancellationTo
if (authenticator != null) await authenticator.Authenticate(this, request).ConfigureAwait(false);
var httpMethod = AsHttpMethod(request.Method);
- var url = BuildUri(request);
+ var url = this.BuildUri(request);
var message = new HttpRequestMessage(httpMethod, url) { Content = requestContent.BuildContent() };
message.Headers.Host = Options.BaseHost;
message.Headers.CacheControl = Options.CachePolicy;
@@ -111,6 +111,11 @@ async Task ExecuteRequestAsync(RestRequest request, CancellationTo
.AddHeaders(DefaultParameters)
.AddAcceptHeader(AcceptedContentTypes)
.AddCookieHeaders(cookieContainer, url);
+
+ if (Options.CookieContainer != null) {
+ headers.AddCookieHeaders(Options.CookieContainer, url);
+ }
+
message.AddHeaders(headers);
if (request.OnBeforeRequest != null) await request.OnBeforeRequest(message).ConfigureAwait(false);
diff --git a/src/RestSharp/RestClient.Extensions.Params.cs b/src/RestSharp/RestClient.Extensions.Params.cs
index 350c088c5..840ef74b3 100644
--- a/src/RestSharp/RestClient.Extensions.Params.cs
+++ b/src/RestSharp/RestClient.Extensions.Params.cs
@@ -13,18 +13,32 @@
// limitations under the License.
//
+using System.Net;
+using System.Text;
+
namespace RestSharp;
public static partial class RestClientExtensions {
+ ///
+ /// Add a parameter to use on every request made with this client instance
+ ///
+ /// instance
+ /// to add
+ ///
+ public static IRestClient AddDefaultParameter(this IRestClient client, Parameter parameter) {
+ client.DefaultParameters.AddParameter(parameter);
+ return client;
+ }
+
///
/// Adds a default HTTP parameter (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT)
/// Used on every request made by this client instance
///
- /// instance
+ /// instance
/// Name of the parameter
/// Value of the parameter
/// This request
- public static RestClient AddDefaultParameter(this RestClient client, string name, string value)
+ public static IRestClient AddDefaultParameter(this IRestClient client, string name, string value)
=> client.AddDefaultParameter(new GetOrPostParameter(name, value));
///
@@ -35,25 +49,22 @@ public static RestClient AddDefaultParameter(this RestClient client, string name
/// - RequestBody: Used by AddBody() (not recommended to use directly)
/// Used on every request made by this client instance
///
- /// instance
+ /// instance
/// Name of the parameter
/// Value of the parameter
/// The type of parameter to add
/// This request
- public static RestClient AddDefaultParameter(this RestClient client, string name, object value, ParameterType type) {
- if (type == ParameterType.RequestBody) throw new ArgumentException("Default parameter cannot be Body", nameof(type));
-
- return client.AddDefaultParameter(Parameter.CreateParameter(name, value, type));
- }
+ public static IRestClient AddDefaultParameter(this IRestClient client, string name, object value, ParameterType type)
+ => client.AddDefaultParameter(Parameter.CreateParameter(name, value, type));
///
/// Adds a default header to the RestClient. Used on every request made by this client instance.
///
- /// instance
+ /// instance
/// Name of the header to add
/// Value of the header to add
///
- public static RestClient AddDefaultHeader(this RestClient client, string name, string value)
+ public static IRestClient AddDefaultHeader(this IRestClient client, string name, string value)
=> client.AddDefaultParameter(new HeaderParameter(name, value));
///
@@ -62,7 +73,7 @@ public static RestClient AddDefaultHeader(this RestClient client, string name, s
/// instance
/// Dictionary containing the Names and Values of the headers to add
///
- public static RestClient AddDefaultHeaders(this RestClient client, Dictionary headers) {
+ public static IRestClient AddDefaultHeaders(this IRestClient client, Dictionary headers) {
foreach (var header in headers) client.AddDefaultParameter(new HeaderParameter(header.Key, header.Value));
return client;
@@ -71,20 +82,20 @@ public static RestClient AddDefaultHeaders(this RestClient client, Dictionary
/// Adds a default URL segment parameter to the RestClient. Used on every request made by this client instance.
///
- /// instance
+ /// instance
/// Name of the segment to add
/// Value of the segment to add
///
- public static RestClient AddDefaultUrlSegment(this RestClient client, string name, string value)
+ public static IRestClient AddDefaultUrlSegment(this IRestClient client, string name, string value)
=> client.AddDefaultParameter(new UrlSegmentParameter(name, value));
///
/// Adds a default URL query parameter to the RestClient. Used on every request made by this client instance.
///
- /// instance
+ /// instance
/// Name of the query parameter to add
/// Value of the query parameter to add
///
- public static RestClient AddDefaultQueryParameter(this RestClient client, string name, string value)
+ public static IRestClient AddDefaultQueryParameter(this IRestClient client, string name, string value)
=> client.AddDefaultParameter(new QueryParameter(name, value));
}
diff --git a/src/RestSharp/RestClient.cs b/src/RestSharp/RestClient.cs
index 00f8b9353..ec1c3f375 100644
--- a/src/RestSharp/RestClient.cs
+++ b/src/RestSharp/RestClient.cs
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Headers;
using RestSharp.Authenticators;
using RestSharp.Serializers;
@@ -35,7 +36,7 @@ public partial class RestClient : IRestClient {
/// Content types that will be sent in the Accept header. The list is populated from the known serializers.
/// If you need to send something else by default, set this property to a different value.
///
- public string[] AcceptedContentTypes { get; set; } = null!;
+ public string[] AcceptedContentTypes { get; set; }
internal HttpClient HttpClient { get; }
@@ -43,12 +44,16 @@ public partial class RestClient : IRestClient {
public ReadOnlyRestClientOptions Options { get; }
/// >
- public RestSerializers Serializers { get; }
+ public RestSerializers Serializers { get; private set; }
+
+ ///
+ public DefaultParameters DefaultParameters { get; }
[Obsolete("Use RestClientOptions.Authenticator instead")]
public IAuthenticator? Authenticator => Options.Authenticator;
// set => Options.Authenticator = value;
+
///
/// Creates an instance of RestClient using the provided
///
@@ -66,8 +71,8 @@ public RestClient(
throw new ArgumentException("BaseUrl must be set when using a client factory");
}
- Serializers = new RestSerializers(ConfigureSerializers(configureSerialization));
- Options = new ReadOnlyRestClientOptions(options);
+ ConfigureSerializers(configureSerialization);
+ Options = new ReadOnlyRestClientOptions(options);
if (useClientFactory) {
_disposeHttpClient = false;
@@ -78,6 +83,8 @@ public RestClient(
HttpClient = GetClient();
}
+ DefaultParameters = new DefaultParameters(Options);
+
HttpClient GetClient() {
var handler = new HttpClientHandler();
ConfigureHttpMessageHandler(handler, Options);
@@ -161,7 +168,7 @@ public RestClient(
bool disposeHttpClient = false,
ConfigureSerialization? configureSerialization = null
) {
- Serializers = new RestSerializers(ConfigureSerializers(configureSerialization));
+ ConfigureSerializers(configureSerialization);
HttpClient = httpClient;
_disposeHttpClient = disposeHttpClient;
@@ -171,7 +178,8 @@ public RestClient(
}
var opt = options ?? new RestClientOptions();
- Options = new ReadOnlyRestClientOptions(opt);
+ Options = new ReadOnlyRestClientOptions(opt);
+ DefaultParameters = new DefaultParameters(Options);
if (options != null) ConfigureHttpClient(httpClient, options);
}
@@ -210,7 +218,8 @@ public RestClient(
static void ConfigureHttpClient(HttpClient httpClient, RestClientOptions options) {
if (options.MaxTimeout > 0) httpClient.Timeout = TimeSpan.FromMilliseconds(options.MaxTimeout);
- if (options.UserAgent != null && httpClient.DefaultRequestHeaders.UserAgent.All(x => $"{x.Product?.Name}/{x.Product?.Version}" != options.UserAgent)) {
+ if (options.UserAgent != null &&
+ httpClient.DefaultRequestHeaders.UserAgent.All(x => $"{x.Product?.Name}/{x.Product?.Version}" != options.UserAgent)) {
httpClient.DefaultRequestHeaders.TryAddWithoutValidation(KnownHeaders.UserAgent, options.UserAgent);
}
@@ -239,77 +248,19 @@ static void ConfigureHttpMessageHandler(HttpClientHandler handler, ReadOnlyRestC
if (options.MaxRedirects.HasValue) handler.MaxAutomaticRedirections = options.MaxRedirects.Value;
}
- public ParametersCollection DefaultParameters { get; } = new();
-
- readonly object _lock = new();
-
- ///
- /// Add a parameter to use on every request made with this client instance
- ///
- /// Parameter to add
- ///
- public RestClient AddDefaultParameter(Parameter parameter) {
- lock (_lock) {
- 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) &&
- DefaultParameters.Any(x => x.Name == parameter.Name)) {
- throw new ArgumentException("A default parameters with the same name has already been added", nameof(parameter));
- }
-
- DefaultParameters.AddParameter(parameter);
- }
-
- return this;
- }
-
- static readonly ParameterType[] MultiParameterTypes = { ParameterType.QueryString, ParameterType.GetOrPost };
-
- internal Uri BuildUri(RestRequest request) {
- DoBuildUriValidations(request);
-
- var (uri, resource) = Options.BaseUrl.GetUrlSegmentParamsValues(request.Resource, Options.Encode, request.Parameters, DefaultParameters);
- var mergedUri = uri.MergeBaseUrlAndResource(resource);
-
- var finalUri = mergedUri.ApplyQueryStringParamsValuesToUri(
- request.Method,
- Options.Encoding,
- Options.EncodeQuery,
- request.Parameters,
- DefaultParameters
- );
- return finalUri;
- }
-
- internal Uri BuildUriWithoutQueryParameters(RestRequest request) {
- DoBuildUriValidations(request);
- var (uri, resource) = Options.BaseUrl.GetUrlSegmentParamsValues(request.Resource, Options.Encode, request.Parameters, DefaultParameters);
- return uri.MergeBaseUrlAndResource(resource);
- }
-
- internal void AssignAcceptedContentTypes(SerializerConfig serializerConfig) => AcceptedContentTypes = serializerConfig.GetAcceptedContentTypes();
-
- void DoBuildUriValidations(RestRequest request) {
- if (Options.BaseUrl == null && !request.Resource.ToLowerInvariant().StartsWith("http"))
- throw new ArgumentOutOfRangeException(
- nameof(request),
- "Request resource doesn't contain a valid scheme for an empty base URL of the client"
- );
- }
-
- SerializerConfig ConfigureSerializers(ConfigureSerialization? configureSerialization) {
- var serializerConfig = new SerializerConfig(this);
+ [MemberNotNull(nameof(Serializers))]
+ [MemberNotNull(nameof(AcceptedContentTypes))]
+ void ConfigureSerializers(ConfigureSerialization? configureSerialization) {
+ var serializerConfig = new SerializerConfig();
serializerConfig.UseDefaultSerializers();
configureSerialization?.Invoke(serializerConfig);
- return serializerConfig;
+ Serializers = new RestSerializers(serializerConfig);
+ AcceptedContentTypes = Serializers.GetAcceptedContentTypes();
}
readonly bool _disposeHttpClient;
- bool _disposed;
+
+ bool _disposed;
protected virtual void Dispose(bool disposing) {
if (disposing && !_disposed) {
diff --git a/src/RestSharp/RestSharp.csproj b/src/RestSharp/RestSharp.csproj
index 7a9f11d0b..28e2b1b0e 100644
--- a/src/RestSharp/RestSharp.csproj
+++ b/src/RestSharp/RestSharp.csproj
@@ -2,9 +2,8 @@
true
-
+
-
diff --git a/src/RestSharp/Serializers/RestSerializers.cs b/src/RestSharp/Serializers/RestSerializers.cs
index c946aaa4e..00bb8de91 100644
--- a/src/RestSharp/Serializers/RestSerializers.cs
+++ b/src/RestSharp/Serializers/RestSerializers.cs
@@ -32,6 +32,8 @@ public IRestSerializer GetSerializer(DataFormat dataFormat)
? value.GetSerializer()
: throw new InvalidOperationException($"Unable to find a serializer for {dataFormat}");
+ internal string[] GetAcceptedContentTypes() => Serializers.SelectMany(x => x.Value.AcceptedContentTypes).Distinct().ToArray();
+
internal RestResponse Deserialize(RestRequest request, RestResponse raw, ReadOnlyRestClientOptions options) {
var response = RestResponse.FromResponse(raw);
diff --git a/src/RestSharp/Serializers/SerializerConfig.cs b/src/RestSharp/Serializers/SerializerConfig.cs
index 986875208..78d1f6921 100644
--- a/src/RestSharp/Serializers/SerializerConfig.cs
+++ b/src/RestSharp/Serializers/SerializerConfig.cs
@@ -19,12 +19,8 @@
namespace RestSharp.Serializers;
public class SerializerConfig {
- internal RestClient Client { get; }
-
internal Dictionary Serializers { get; } = new();
- internal SerializerConfig(RestClient client) => Client = client;
-
///
/// Replace the default serializer with a custom one
///
@@ -38,7 +34,6 @@ public SerializerConfig UseSerializer(Func serializerFactory) {
instance.SupportsContentType,
serializerFactory
);
- Client.AssignAcceptedContentTypes(this);
return this;
}
@@ -50,8 +45,6 @@ public SerializerConfig UseSerializer(Func serializerFactory) {
/// The type that implements
///
public SerializerConfig UseSerializer() where T : class, IRestSerializer, new() => UseSerializer(() => new T());
-
- internal string[] GetAcceptedContentTypes() => Serializers.SelectMany(x => x.Value.AcceptedContentTypes).Distinct().ToArray();
}
public static class SerializerConfigExtensions {
@@ -62,7 +55,6 @@ public static class SerializerConfigExtensions {
/// Reference to the client instance
public static SerializerConfig UseJson(this SerializerConfig config) {
config.Serializers.Remove(DataFormat.Xml);
- config.Client.AssignAcceptedContentTypes(config);
return config;
}
@@ -73,7 +65,6 @@ public static SerializerConfig UseJson(this SerializerConfig config) {
/// Reference to the client instance
public static SerializerConfig UseXml(this SerializerConfig config) {
config.Serializers.Remove(DataFormat.Json);
- config.Client.AssignAcceptedContentTypes(config);
return config;
}
diff --git a/src/RestSharp/Serializers/Xml/DotNetXmlSerializer.cs b/src/RestSharp/Serializers/Xml/DotNetXmlSerializer.cs
index dd9c8407a..20eda856e 100644
--- a/src/RestSharp/Serializers/Xml/DotNetXmlSerializer.cs
+++ b/src/RestSharp/Serializers/Xml/DotNetXmlSerializer.cs
@@ -25,7 +25,7 @@ public class DotNetXmlSerializer : IXmlSerializer {
/// Default constructor, does not specify namespace
///
public DotNetXmlSerializer() {
- ContentType = RestSharp.ContentType.Xml;
+ ContentType = ContentType.Xml;
Encoding = Encoding.UTF8;
}
@@ -34,6 +34,7 @@ public DotNetXmlSerializer() {
/// Specify the namespaced to be used when serializing
///
/// XML namespace
+ [PublicAPI]
public DotNetXmlSerializer(string @namespace) : this() => Namespace = @namespace;
///
@@ -53,7 +54,7 @@ public string Serialize(object obj) {
var root = RootElement == null ? null : new XmlRootAttribute(RootElement);
- var serializer = new XmlSerializer(obj.GetType(), root);
+ var serializer = GetXmlSerializer(obj.GetType(), RootElement);
var writer = new EncodingStringWriter(Encoding);
serializer.Serialize(writer, obj, ns);
@@ -71,20 +72,58 @@ public string Serialize(object obj) {
///
public string? Namespace { get; set; }
- ///
- /// Format string to use when serializing dates
- ///
- public string? DateFormat { get; set; }
-
///
/// Content type for serialized content
///
public ContentType ContentType { get; set; }
+ static readonly Dictionary<(Type, string?), XmlSerializer> Cache = new();
+ static readonly ReaderWriterLockSlim CacheLock = new();
+
+ static XmlSerializer GetXmlSerializer(Type type, string? rootElement) {
+ XmlSerializer? serializer = null;
+
+ var key = (type, rootElement);
+
+ CacheLock.EnterReadLock();
+
+ try {
+ if (Cache.ContainsKey(key)) {
+ serializer = Cache[key];
+ }
+ }
+ finally {
+ CacheLock.ExitReadLock();
+ }
+
+ if (serializer != null) {
+ return serializer;
+ }
+
+ CacheLock.EnterWriteLock();
+
+ try {
+ // check again for a cached instance, because between the EnterWriteLock
+ // and the last check, some other thread could have added an instance
+ if (!Cache.ContainsKey(key)) {
+ var root = rootElement == null ? null : new XmlRootAttribute(rootElement);
+
+ Cache[key] = new XmlSerializer(type, root);
+ }
+
+ serializer = Cache[key];
+ }
+ finally {
+ CacheLock.ExitWriteLock();
+ }
+
+ return serializer;
+ }
+
class EncodingStringWriter : StringWriter {
// Need to subclass StringWriter in order to override Encoding
public EncodingStringWriter(Encoding encoding) => Encoding = encoding;
public override Encoding Encoding { get; }
}
-}
\ No newline at end of file
+}
diff --git a/test/Directory.Build.props b/test/Directory.Build.props
index b0d12e689..e52f2fcc2 100644
--- a/test/Directory.Build.props
+++ b/test/Directory.Build.props
@@ -14,8 +14,8 @@
-
-
+
+
diff --git a/test/RestSharp.Tests.Integrated/HttpHeadersTests.cs b/test/RestSharp.Tests.Integrated/HttpHeadersTests.cs
index 0e19c5d38..d5714eadd 100644
--- a/test/RestSharp.Tests.Integrated/HttpHeadersTests.cs
+++ b/test/RestSharp.Tests.Integrated/HttpHeadersTests.cs
@@ -1,24 +1,26 @@
+using System.Net;
using RestSharp.Tests.Integrated.Fixtures;
+using RestSharp.Tests.Integrated.Server;
using RestSharp.Tests.Shared.Fixtures;
namespace RestSharp.Tests.Integrated;
-public class HttpHeadersTests : CaptureFixture {
+[Collection(nameof(TestServerCollection))]
+public class HttpHeadersTests {
readonly ITestOutputHelper _output;
+ readonly RestClient _client;
- public HttpHeadersTests(ITestOutputHelper output) => _output = output;
+ public HttpHeadersTests(TestServerFixture fixture, ITestOutputHelper output) {
+ _output = output;
+ _client = new RestClient(new RestClientOptions(fixture.Server.Url) { ThrowOnAnyError = true });
+ }
[Fact]
public async Task Ensure_headers_correctly_set_in_the_hook() {
const string headerName = "HeaderName";
const string headerValue = "HeaderValue";
- using var server = SimpleServer.Create(Handlers.Generic());
-
- // Prepare
- var client = new RestClient(server.Url);
-
- var request = new RestRequest(RequestHeadCapturer.Resource) {
+ var request = new RestRequest("/headers") {
OnBeforeRequest = http => {
http.Headers.Add(headerName, headerValue);
return default;
@@ -26,9 +28,35 @@ public async Task Ensure_headers_correctly_set_in_the_hook() {
};
// Run
- await client.ExecuteAsync(request);
+ var response = await _client.ExecuteAsync(request);
// Assert
- RequestHeadCapturer.CapturedHeaders[headerName].Should().Be(headerValue);
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+ var header = response.Data!.First(x => x.Name == headerName);
+ header.Should().NotBeNull();
+ header.Value.Should().Be(headerValue);
}
+
+ [Fact]
+ public async Task Should_use_both_default_and_request_headers() {
+ var defaultHeader = new Header("defName", "defValue");
+ var requestHeader = new Header("reqName", "reqValue");
+
+ _client.AddDefaultHeader(defaultHeader.Name, defaultHeader.Value);
+
+ var request = new RestRequest("/headers")
+ .AddHeader(requestHeader.Name, requestHeader.Value);
+
+ var response = await _client.ExecuteAsync(request);
+ CheckHeader(defaultHeader);
+ CheckHeader(requestHeader);
+
+ void CheckHeader(Header header) {
+ var h = response.Data!.First(x => x.Name == header.Name);
+ h.Should().NotBeNull();
+ h.Value.Should().Be(header.Value);
+ }
+ }
+
+ record Header(string Name, string Value);
}
\ No newline at end of file
diff --git a/test/RestSharp.Tests.Integrated/Models/LinkedINMemberProfile.cs b/test/RestSharp.Tests.Integrated/Models/LinkedINMemberProfile.cs
deleted file mode 100644
index 758ff77d3..000000000
--- a/test/RestSharp.Tests.Integrated/Models/LinkedINMemberProfile.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace RestSharp.Tests.Integrated.Models;
-
-///
-/// Model for used by the LinkedIN integration tests.
-/// .
-///
-public class LinkedInMemberProfile {
- public string Id { get; set; }
-
- public string FirstName { get; set; }
-
- public string LastName { get; set; }
-}
\ No newline at end of file
diff --git a/test/RestSharp.Tests.Integrated/MultipartFormDataTests.cs b/test/RestSharp.Tests.Integrated/MultipartFormDataTests.cs
index 4ea0e3c8b..0004593da 100644
--- a/test/RestSharp.Tests.Integrated/MultipartFormDataTests.cs
+++ b/test/RestSharp.Tests.Integrated/MultipartFormDataTests.cs
@@ -2,6 +2,8 @@
using HttpTracer;
using RestSharp.Tests.Integrated.Fixtures;
using RestSharp.Tests.Shared.Fixtures;
+#pragma warning disable CS8618
+#pragma warning disable CS8601
namespace RestSharp.Tests.Integrated;
@@ -32,13 +34,13 @@ public MultipartFormDataTests(ITestOutputHelper output) {
$"--{{0}}--{LineBreak}";
const string ExpectedFileAndBodyRequestContent =
- "--{0}" +
- $"{LineBreak}{KnownHeaders.ContentType}: application/octet-stream" +
+ "--{0}" +
+ $"{LineBreak}{KnownHeaders.ContentType}: application/octet-stream" +
$"{LineBreak}{KnownHeaders.ContentDisposition}: form-data; name=\"fileName\"; filename=\"TestFile.txt\"" +
- $"{LineBreak}{LineBreak}This is a test file for RestSharp.{LineBreak}" +
- $"--{{0}}{LineBreak}{KnownHeaders.ContentType}: application/json; {CharsetString}" +
- $"{LineBreak}{KnownHeaders.ContentDisposition}: form-data; name=controlName" +
- $"{LineBreak}{LineBreak}test{LineBreak}" +
+ $"{LineBreak}{LineBreak}This is a test file for RestSharp.{LineBreak}" +
+ $"--{{0}}{LineBreak}{KnownHeaders.ContentType}: application/json; {CharsetString}" +
+ $"{LineBreak}{KnownHeaders.ContentDisposition}: form-data; name=controlName" +
+ $"{LineBreak}{LineBreak}test{LineBreak}" +
$"--{{0}}--{LineBreak}";
const string ExpectedDefaultMultipartContentType = "multipart/form-data; boundary=\"{0}\"";
@@ -76,11 +78,24 @@ public async Task AlwaysMultipartFormData_WithParameter_Execute() {
Assert.Null(response.ErrorException);
}
+ [Fact]
+ public async Task MultipartFormData_NoBoundaryQuotes() {
+ var request = new RestRequest("/", Method.Post) { AlwaysMultipartFormData = true };
+
+ AddParameters(request);
+ request.MultipartFormQuoteBoundary = false;
+
+ var response = await _client.ExecuteAsync(request);
+
+ var expected = string.Format(Expected, request.FormBoundary);
+
+ response.Content.Should().Be(expected);
+ RequestHandler.CapturedContentType.Should().Be($"multipart/form-data; boundary={request.FormBoundary}");
+ }
+
[Fact]
public async Task MultipartFormData() {
- var request = new RestRequest("/", Method.Post) {
- AlwaysMultipartFormData = true
- };
+ var request = new RestRequest("/", Method.Post) { AlwaysMultipartFormData = true };
AddParameters(request);
@@ -91,7 +106,8 @@ public async Task MultipartFormData() {
_output.WriteLine($"Expected: {expected}");
_output.WriteLine($"Actual: {response.Content}");
- Assert.Equal(expected, response.Content);
+ response.Content.Should().Be(expected);
+ RequestHandler.CapturedContentType.Should().Be($"multipart/form-data; boundary=\"{request.FormBoundary}\"");
}
[Fact]
@@ -187,4 +203,4 @@ public async Task ShouldHaveJsonContentType() {
var response = await _client.ExecuteAsync(request);
}
-}
\ No newline at end of file
+}
diff --git a/test/RestSharp.Tests.Integrated/NonProtocolExceptionHandlingTests.cs b/test/RestSharp.Tests.Integrated/NonProtocolExceptionHandlingTests.cs
index 8ebb3c4a5..5c31e8a25 100644
--- a/test/RestSharp.Tests.Integrated/NonProtocolExceptionHandlingTests.cs
+++ b/test/RestSharp.Tests.Integrated/NonProtocolExceptionHandlingTests.cs
@@ -6,7 +6,7 @@ namespace RestSharp.Tests.Integrated;
public sealed class NonProtocolExceptionHandlingTests : IDisposable {
// ReSharper disable once ClassNeverInstantiated.Local
class StupidClass {
- public string Property { get; set; }
+ public string Property { get; set; } = null!;
}
///
diff --git a/test/RestSharp.Tests.Integrated/PostTests.cs b/test/RestSharp.Tests.Integrated/PostTests.cs
index a37a454a6..e3482013f 100644
--- a/test/RestSharp.Tests.Integrated/PostTests.cs
+++ b/test/RestSharp.Tests.Integrated/PostTests.cs
@@ -47,7 +47,32 @@ public async Task Should_post_large_form_data() {
response.Data!.Message.Should().Be($"Works! Length: {length}");
}
+ [Fact]
+ public async Task Should_post_both_default_and_request_parameters() {
+ var defParam = new PostParameter("default", "default");
+ var reqParam = new PostParameter("request", "request");
+
+ _client.AddDefaultParameter(defParam.Name, defParam.Value);
+
+ var request = new RestRequest("post/data")
+ .AddParameter(reqParam.Name, reqParam.Value);
+
+ var response = await _client.ExecutePostAsync(request);
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ CheckResponse(defParam);
+ CheckResponse(reqParam);
+
+ void CheckResponse(PostParameter parameter) {
+ var p = response.Data!.FirstOrDefault(x => x.Name == parameter.Name);
+ p.Should().NotBeNull();
+ p!.Value.Should().Be(parameter.Value);
+ }
+ }
+
class Response {
- public string Message { get; set; }
+ public string Message { get; set; } = null!;
}
+
+ record PostParameter(string Name, string Value);
}
diff --git a/test/RestSharp.Tests.Integrated/RequestFailureTests.cs b/test/RestSharp.Tests.Integrated/RequestFailureTests.cs
index 634c9d9e3..13f2557c2 100644
--- a/test/RestSharp.Tests.Integrated/RequestFailureTests.cs
+++ b/test/RestSharp.Tests.Integrated/RequestFailureTests.cs
@@ -73,6 +73,6 @@ public async Task GetAsync_returns_null_on_404() {
}
class Response {
- public string Message { get; set; }
+ public string Message { get; set; } = null!;
}
}
\ No newline at end of file
diff --git a/test/RestSharp.Tests.Integrated/RequestHeadTests.cs b/test/RestSharp.Tests.Integrated/RequestHeadTests.cs
index a69cc5b9b..1c5d98eb1 100644
--- a/test/RestSharp.Tests.Integrated/RequestHeadTests.cs
+++ b/test/RestSharp.Tests.Integrated/RequestHeadTests.cs
@@ -54,7 +54,7 @@ public async Task Passes_Default_Credentials_When_UseDefaultCredentials_Is_True(
response.StatusCode.ToString().Should().BeOneOf(HttpStatusCode.OK.ToString(),HttpStatusCode.Unauthorized.ToString());
RequestHeadCapturer.CapturedHeaders.Should().NotBeNull();
- var keys = RequestHeadCapturer.CapturedHeaders.Keys.Cast().ToArray();
+ var keys = RequestHeadCapturer.CapturedHeaders!.Keys.Cast().ToArray();
keys.Should()
.Contain(
diff --git a/test/RestSharp.Tests.Integrated/RequestTests.cs b/test/RestSharp.Tests.Integrated/RequestTests.cs
index 312059004..98c04e65e 100644
--- a/test/RestSharp.Tests.Integrated/RequestTests.cs
+++ b/test/RestSharp.Tests.Integrated/RequestTests.cs
@@ -16,7 +16,7 @@ public AsyncTests(TestServerFixture fixture, ITestOutputHelper output) {
}
class Response {
- public string Message { get; set; }
+ public string Message { get; set; } = null!;
}
[Fact]
diff --git a/test/RestSharp.Tests.Integrated/ResourceStringParametersTests.cs b/test/RestSharp.Tests.Integrated/ResourceStringParametersTests.cs
index 4baf0c280..f214a38db 100644
--- a/test/RestSharp.Tests.Integrated/ResourceStringParametersTests.cs
+++ b/test/RestSharp.Tests.Integrated/ResourceStringParametersTests.cs
@@ -23,6 +23,7 @@ public async Task Should_keep_to_parameters_with_the_same_name() {
query.Should().Be(parameters);
}
+ #nullable disable
static class RequestHandler {
public static Uri Url { get; private set; }
@@ -31,4 +32,5 @@ public static void Handle(HttpListenerContext context) {
Handlers.Echo(context);
}
}
+ #nullable enable
}
\ 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 f084e1ecb..0fe9a8e69 100644
--- a/test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj
+++ b/test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj
@@ -23,4 +23,7 @@
+
+
+
\ No newline at end of file
diff --git a/test/RestSharp.Tests.Integrated/Server/Handlers/FileHandlers.cs b/test/RestSharp.Tests.Integrated/Server/Handlers/FileHandlers.cs
index d65f843b0..7d7231e9e 100644
--- a/test/RestSharp.Tests.Integrated/Server/Handlers/FileHandlers.cs
+++ b/test/RestSharp.Tests.Integrated/Server/Handlers/FileHandlers.cs
@@ -45,5 +45,5 @@ public async Task Upload([FromForm] FormFile formFile) {
}
public class FormFile {
- public IFormFile File { get; set; }
+ public IFormFile File { get; set; } = null!;
}
diff --git a/test/RestSharp.Tests.Integrated/Server/Handlers/FormRequest.cs b/test/RestSharp.Tests.Integrated/Server/Handlers/FormRequest.cs
new file mode 100644
index 000000000..a70e88117
--- /dev/null
+++ b/test/RestSharp.Tests.Integrated/Server/Handlers/FormRequest.cs
@@ -0,0 +1,12 @@
+using Microsoft.AspNetCore.Http;
+
+namespace RestSharp.Tests.Integrated.Server.Handlers;
+
+public static class FormRequestHandler {
+ public static IResult HandleForm(HttpContext ctx) {
+ var response = ctx.Request.Form.Select(
+ x => new TestServerResponse(x.Key, x.Value!)
+ );
+ return Results.Ok(response);
+ }
+}
diff --git a/test/RestSharp.Tests.Integrated/Server/Handlers/HeaderHandlers.cs b/test/RestSharp.Tests.Integrated/Server/Handlers/HeaderHandlers.cs
index 5339052a8..d687be698 100644
--- a/test/RestSharp.Tests.Integrated/Server/Handlers/HeaderHandlers.cs
+++ b/test/RestSharp.Tests.Integrated/Server/Handlers/HeaderHandlers.cs
@@ -4,7 +4,7 @@ namespace RestSharp.Tests.Integrated.Server.Handlers;
public static class HeaderHandlers {
public static IResult HandleHeaders(HttpContext ctx) {
- var response = ctx.Request.Headers.Select(x => new TestServerResponse(x.Key, x.Value));
+ var response = ctx.Request.Headers.Select(x => new TestServerResponse(x.Key, x.Value!));
return Results.Ok(response);
}
}
diff --git a/test/RestSharp.Tests.Integrated/Server/Handlers/RequestHandlers.cs b/test/RestSharp.Tests.Integrated/Server/Handlers/RequestHandlers.cs
index 43cda8d30..af34c0d73 100644
--- a/test/RestSharp.Tests.Integrated/Server/Handlers/RequestHandlers.cs
+++ b/test/RestSharp.Tests.Integrated/Server/Handlers/RequestHandlers.cs
@@ -13,7 +13,7 @@ public ParsedRequest(HttpRequest request) {
QueryString = request.QueryString;
QueryParameters = request.Query
- .SelectMany(x => x.Value.Select(y => new KeyValuePair(x.Key, y)))
+ .SelectMany(x => x.Value.Select(y => new KeyValuePair(x.Key, y!)))
.ToArray();
}
diff --git a/test/RestSharp.Tests.Integrated/Server/TestServer.cs b/test/RestSharp.Tests.Integrated/Server/TestServer.cs
index df9307e07..6b34ad7d8 100644
--- a/test/RestSharp.Tests.Integrated/Server/TestServer.cs
+++ b/test/RestSharp.Tests.Integrated/Server/TestServer.cs
@@ -20,7 +20,7 @@ public sealed class HttpServer {
public HttpServer(ITestOutputHelper? output = null) {
var builder = WebApplication.CreateBuilder();
- if (output != null) builder.WebHost.ConfigureLogging(x => x.SetMinimumLevel(LogLevel.Information).AddXunit(output, LogLevel.Debug));
+ if (output != null) builder.Logging.AddXunit(output, LogLevel.Debug);
builder.Services.AddControllers().AddApplicationPart(typeof(UploadController).Assembly);
builder.WebHost.UseUrls(Address);
@@ -61,6 +61,8 @@ public HttpServer(ITestOutputHelper? output = null) {
"/post/form",
(HttpContext context) => new TestResponse { Message = $"Works! Length: {context.Request.Form["big_string"].ToString().Length}" }
);
+
+ _app.MapPost("/post/data", FormRequestHandler.HandleForm);
}
public Uri Url => new(Address);
diff --git a/test/RestSharp.Tests.Integrated/StatusCodeTests.cs b/test/RestSharp.Tests.Integrated/StatusCodeTests.cs
index c9fbb2e77..6ba326fc8 100644
--- a/test/RestSharp.Tests.Integrated/StatusCodeTests.cs
+++ b/test/RestSharp.Tests.Integrated/StatusCodeTests.cs
@@ -1,5 +1,4 @@
using System.Net;
-using RestSharp.Serializers;
using RestSharp.Serializers.Xml;
using RestSharp.Tests.Shared.Extensions;
using RestSharp.Tests.Shared.Fixtures;
@@ -57,7 +56,7 @@ public async Task Handles_Default_Root_Element_On_No_Error() {
var response = await _client.ExecuteAsync(request);
response.StatusCode.Should().Be(HttpStatusCode.OK);
- response.Data.Message.Should().Be("Works!");
+ response.Data!.Message.Should().Be("Works!");
}
[Fact]
@@ -74,7 +73,7 @@ public async Task Handles_Different_Root_Element_On_Http_Error() {
var response = await _client.ExecuteAsync(request);
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
- response.Data.Message.Should().Be("Not found!");
+ response.Data!.Message.Should().Be("Not found!");
}
[Fact]
@@ -178,5 +177,5 @@ void success(HttpListenerContext context)
}
public class TestResponse {
- public string Message { get; set; }
+ public string Message { get; set; } = null!;
}
diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/twitter.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/twitter.cs
index 53cdb83b1..ec0af5146 100644
--- a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/twitter.cs
+++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/twitter.cs
@@ -1,8 +1,12 @@
using RestSharp.Serializers;
+// ReSharper disable InconsistentNaming
+#pragma warning disable CS8981
namespace RestSharp.Tests.Serializers.Xml.SampleClasses;
+#pragma warning disable CS8981
public class status {
+#pragma warning restore CS8981
public bool truncated { get; set; }
public string created_at { get; set; }