Skip to content

Commit b9753f6

Browse files
committed
Added a simple factory with opt-in
1 parent d284f78 commit b9753f6

File tree

14 files changed

+162
-74
lines changed

14 files changed

+162
-74
lines changed

docs/v107/interfaces.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@
2828
| `UseSerializer(Func<IRestSerializer> serializerFactory)` | `RestClient` |
2929
| `UseSerializer<T>()` | `RestClient` |
3030
| `Deserialize<T>(IRestResponse response)` | `RestClient` |
31-
| `BuildUri(IRestRequest request)` | `RestClient` |
32-
| `UseUrlEncoder(Func<string, string> encoder)` | Extension |
33-
| `UseQueryEncoder(Func<string, Encoding, string> queryEncoder)` | Extension |
31+
| `UseUrlEncoder(Func<string, string> encoder)` | `RestClientOptions.Encode` |
32+
| `UseQueryEncoder(Func<string, Encoding, string> queryEncoder)` | `RestClientOptions.EncodeQuery` |
3433
| `ExecuteAsync<T>(IRestRequest request, CancellationToken cancellationToken)` | `RestClient` |
3534
| `ExecuteAsync<T>(IRestRequest request, Method httpMethod, CancellationToken cancellationToken)` | Extension |
3635
| `ExecuteAsync(IRestRequest request, Method httpMethod, CancellationToken cancellationToken)` | Extension |

src/RestSharp/Options/RestClientOptions.cs

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,14 @@ public RestClientOptions() { }
3636

3737
public RestClientOptions(string baseUrl) : this(new Uri(Ensure.NotEmptyString(baseUrl, nameof(baseUrl)))) { }
3838

39-
/// <inheritdoc/>
39+
/// <summary>
40+
/// Base URL for all requests made with this client instance
41+
/// </summary>
4042
public Uri? BaseUrl { get; set; }
4143

44+
/// <summary>
45+
/// Custom configuration for the underlying <seealso cref="HttpMessageHandler"/>
46+
/// </summary>
4247
public Func<HttpMessageHandler, HttpMessageHandler>? ConfigureMessageHandler { get; set; }
4348

4449
/// <summary>
@@ -49,15 +54,10 @@ public RestClientOptions(string baseUrl) : this(new Uri(Ensure.NotEmptyString(ba
4954
? ResponseStatus.Completed
5055
: ResponseStatus.Error;
5156

52-
volatile IAuthenticator? _authenticator;
53-
5457
/// <summary>
5558
/// Authenticator that will be used to populate request with necessary authentication data
5659
/// </summary>
57-
public IAuthenticator? Authenticator {
58-
get => _authenticator;
59-
set => _authenticator = value;
60-
}
60+
public IAuthenticator? Authenticator { get; set; }
6161

6262
/// <summary>
6363
/// Passed to <see cref="HttpMessageHandler"/> <code>Credentials</code> property
@@ -76,27 +76,52 @@ public IAuthenticator? Authenticator {
7676
/// </summary>
7777
public bool DisableCharset { get; set; }
7878

79+
/// <summary>
80+
/// Set the decompression method to use when making requests
81+
/// </summary>
7982
#if NET
8083
public DecompressionMethods AutomaticDecompression { get; set; } = DecompressionMethods.All;
8184
#else
8285
public DecompressionMethods AutomaticDecompression { get; set; } = DecompressionMethods.GZip;
8386
#endif
8487

88+
/// <summary>
89+
/// Set the maximum number of redirects to follow
90+
/// </summary>
8591
public int? MaxRedirects { get; set; }
8692

8793
/// <summary>
8894
/// X509CertificateCollection to be sent with request
8995
/// </summary>
9096
public X509CertificateCollection? ClientCertificates { get; set; }
9197

92-
public IWebProxy? Proxy { get; set; }
93-
public CacheControlHeaderValue? CachePolicy { get; set; }
94-
public bool FollowRedirects { get; set; } = true;
95-
public bool? Expect100Continue { get; set; } = null;
96-
public string? UserAgent { get; set; } = DefaultUserAgent;
98+
/// <summary>
99+
/// Set the proxy to use when making requests. Default is null, which will use the default system proxy if one is set.
100+
/// </summary>
101+
public IWebProxy? Proxy { get; set; }
102+
103+
/// <summary>
104+
/// Cache policy to be used for requests using <seealso cref="CacheControlHeaderValue"/>
105+
/// </summary>
106+
public CacheControlHeaderValue? CachePolicy { get; set; }
107+
108+
/// <summary>
109+
/// Instruct the client to follow redirects. Default is true.
110+
/// </summary>
111+
public bool FollowRedirects { get; set; } = true;
112+
113+
/// <summary>
114+
/// Gets or sets a value that indicates if the <see langword="Expect" /> header for an HTTP request contains Continue.
115+
/// </summary>
116+
public bool? Expect100Continue { get; set; } = null;
97117

98118
/// <summary>
99-
/// Passed to <see cref="HttpMessageHandler"/> <code>PreAuthenticate</code> property
119+
/// Value of the User-Agent header to be sent with requests. Default is "RestSharp/{version}"
120+
/// </summary>
121+
public string? UserAgent { get; set; } = DefaultUserAgent;
122+
123+
/// <summary>
124+
/// Passed to <see cref="HttpMessageHandler"/> <see langword="PreAuthenticate"/> property
100125
/// </summary>
101126
public bool PreAuthenticate { get; set; }
102127

@@ -106,6 +131,9 @@ public IAuthenticator? Authenticator {
106131
/// </summary>
107132
public RemoteCertificateValidationCallback? RemoteCertificateValidationCallback { get; set; }
108133

134+
/// <summary>
135+
/// Sets the value of the Host header to be sent with requests.
136+
/// </summary>
109137
public string? BaseHost { get; set; }
110138

111139
/// <summary>
@@ -114,23 +142,40 @@ public IAuthenticator? Authenticator {
114142
/// </summary>
115143
public int MaxTimeout { get; set; }
116144

145+
/// <summary>
146+
/// Default encoding to use when no encoding is specified in the content type header.
147+
/// </summary>
117148
public Encoding Encoding { get; set; } = Encoding.UTF8;
118149

119-
/// <inheritdoc />>
150+
/// <summary>
151+
/// Set to true to throw an exception when a deserialization error occurs. Default is false.
152+
/// </summary>
120153
public bool ThrowOnDeserializationError { get; set; }
121154

122-
/// <inheritdoc />>
155+
/// <summary>
156+
/// When set to true, the response status will be set to <see cref="ResponseStatus.Error"/>
157+
/// when a deserialization error occurs. Default is true.
158+
/// </summary>
123159
public bool FailOnDeserializationError { get; set; } = true;
124160

125-
/// <inheritdoc />>
161+
/// <summary>
162+
/// Set to true to throw an exception when <seealso cref="HttpClient"/> throws an exception when making a request.
163+
/// Default is false.
164+
/// </summary>
126165
public bool ThrowOnAnyError { get; set; }
127166

128-
/// <inheritdoc />>
167+
/// <summary>
168+
/// Set to true to allow multiple default parameters with the same name. Default is false.
169+
/// </summary>
129170
public bool AllowMultipleDefaultParametersWithSameName { get; set; }
130171

131-
/// <inheritdoc />>
172+
/// <summary>
173+
/// Custom function to encode a string for use in a URL.
174+
/// </summary>
132175
public Func<string, string> Encode { get; set; } = s => s.UrlEncode();
133176

134-
/// <inheritdoc />>
177+
/// <summary>
178+
/// Custom function to encode a string for use in a URL query.
179+
/// </summary>
135180
public Func<string, Encoding, string> EncodeQuery { get; set; } = (s, encoding) => s.UrlEncode(encoding)!;
136181
}

src/RestSharp/RestClient.Async.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public async Task<RestResponse> ExecuteAsync(RestRequest request, CancellationTo
2727
internalResponse.ResponseMessage!,
2828
request,
2929
Options.Encoding,
30-
request.CookieContainer?.GetCookies(internalResponse.Url),
30+
internalResponse.CookieContainer?.GetCookies(internalResponse.Url),
3131
Options.CalculateResponseStatus,
3232
cancellationToken
3333
)
@@ -131,14 +131,20 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
131131

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

134-
return new HttpResponse(responseMessage, url, null, timeoutCts.Token);
134+
return new HttpResponse(responseMessage, url, cookieContainer, null, timeoutCts.Token);
135135
}
136136
catch (Exception ex) {
137-
return new HttpResponse(null, url, ex, timeoutCts.Token);
137+
return new HttpResponse(null, url, null, ex, timeoutCts.Token);
138138
}
139139
}
140140

141-
record HttpResponse(HttpResponseMessage? ResponseMessage, Uri Url, Exception? Exception, CancellationToken TimeoutToken);
141+
record HttpResponse(
142+
HttpResponseMessage? ResponseMessage,
143+
Uri Url,
144+
CookieContainer? CookieContainer,
145+
Exception? Exception,
146+
CancellationToken TimeoutToken
147+
);
142148

143149
static HttpMethod AsHttpMethod(Method method)
144150
=> method switch {

src/RestSharp/RestClient.cs

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public partial class RestClient : IRestClient {
3737
/// </summary>
3838
public string[] AcceptedContentTypes { get; set; } = null!;
3939

40-
HttpClient HttpClient { get; }
40+
internal HttpClient HttpClient { get; }
4141

4242
/// <inheritdoc />>
4343
public ReadOnlyRestClientOptions Options { get; }
@@ -55,24 +55,39 @@ public partial class RestClient : IRestClient {
5555
/// <param name="options">Client options</param>
5656
/// <param name="configureDefaultHeaders">Delegate to add default headers to the wrapped HttpClient instance</param>
5757
/// <param name="configureSerialization">Delegate to configure serialization</param>
58+
/// <param name="useClientFactory">Set to true if you wish to reuse the <seealso cref="HttpClient"/> instance</param>
5859
public RestClient(
5960
RestClientOptions options,
6061
ConfigureHeaders? configureDefaultHeaders = null,
61-
ConfigureSerialization? configureSerialization = null
62+
ConfigureSerialization? configureSerialization = null,
63+
bool useClientFactory = false
6264
) {
63-
Serializers = new RestSerializers(ConfigureSerializers(configureSerialization));
65+
if (useClientFactory && options.BaseUrl == null) {
66+
throw new ArgumentException("BaseUrl must be set when using a client factory");
67+
}
6468

65-
Options = new ReadOnlyRestClientOptions(options);
66-
_disposeHttpClient = true;
69+
Serializers = new RestSerializers(ConfigureSerializers(configureSerialization));
70+
Options = new ReadOnlyRestClientOptions(options);
6771

68-
var handler = new HttpClientHandler();
69-
ConfigureHttpMessageHandler(handler, Options);
72+
if (useClientFactory) {
73+
_disposeHttpClient = false;
74+
HttpClient = SimpleClientFactory.GetClient(options.BaseUrl!, GetClient);
75+
}
76+
else {
77+
_disposeHttpClient = true;
78+
HttpClient = GetClient();
79+
}
7080

71-
var finalHandler = options.ConfigureMessageHandler?.Invoke(handler) ?? handler;
81+
HttpClient GetClient() {
82+
var handler = new HttpClientHandler();
83+
ConfigureHttpMessageHandler(handler, Options);
84+
var finalHandler = options.ConfigureMessageHandler?.Invoke(handler) ?? handler;
7285

73-
HttpClient = new HttpClient(finalHandler);
74-
ConfigureHttpClient();
75-
configureDefaultHeaders?.Invoke(HttpClient.DefaultRequestHeaders);
86+
var httpClient = new HttpClient(finalHandler);
87+
ConfigureHttpClient(httpClient, options);
88+
configureDefaultHeaders?.Invoke(httpClient.DefaultRequestHeaders);
89+
return httpClient;
90+
}
7691
}
7792

7893
static RestClientOptions ConfigureOptions(RestClientOptions options, ConfigureRestClient? configureRestClient) {
@@ -86,12 +101,14 @@ static RestClientOptions ConfigureOptions(RestClientOptions options, ConfigureRe
86101
/// <param name="configureRestClient">Delegate to configure the client options</param>
87102
/// <param name="configureDefaultHeaders">Delegate to add default headers to the wrapped HttpClient instance</param>
88103
/// <param name="configureSerialization">Delegate to configure serialization</param>
104+
/// <param name="useClientFactory">Set to true if you wish to reuse the <seealso cref="HttpClient"/> instance</param>
89105
public RestClient(
90106
ConfigureRestClient? configureRestClient = null,
91107
ConfigureHeaders? configureDefaultHeaders = null,
92-
ConfigureSerialization? configureSerialization = null
108+
ConfigureSerialization? configureSerialization = null,
109+
bool useClientFactory = false
93110
)
94-
: this(ConfigureOptions(new RestClientOptions(), configureRestClient), configureDefaultHeaders, configureSerialization) { }
111+
: this(ConfigureOptions(new RestClientOptions(), configureRestClient), configureDefaultHeaders, configureSerialization, useClientFactory) { }
95112

96113
/// <inheritdoc />
97114
/// <summary>
@@ -101,16 +118,19 @@ public RestClient(
101118
/// <param name="configureRestClient">Delegate to configure the client options</param>
102119
/// <param name="configureDefaultHeaders">Delegate to add default headers to the wrapped HttpClient instance</param>
103120
/// <param name="configureSerialization">Delegate to configure serialization</param>
121+
/// <param name="useClientFactory">Set to true if you wish to reuse the <seealso cref="HttpClient"/> instance</param>
104122
public RestClient(
105123
Uri baseUrl,
106124
ConfigureRestClient? configureRestClient = null,
107125
ConfigureHeaders? configureDefaultHeaders = null,
108-
ConfigureSerialization? configureSerialization = null
126+
ConfigureSerialization? configureSerialization = null,
127+
bool useClientFactory = false
109128
)
110129
: this(
111130
ConfigureOptions(new RestClientOptions { BaseUrl = baseUrl }, configureRestClient),
112131
configureDefaultHeaders,
113-
configureSerialization
132+
configureSerialization,
133+
useClientFactory
114134
) { }
115135

116136
/// <summary>
@@ -153,7 +173,7 @@ public RestClient(
153173
var opt = options ?? new RestClientOptions();
154174
Options = new ReadOnlyRestClientOptions(opt);
155175

156-
if (options != null) ConfigureHttpClient();
176+
if (options != null) ConfigureHttpClient(httpClient, options);
157177
}
158178

159179
/// <summary>
@@ -187,14 +207,14 @@ public RestClient(
187207
)
188208
: this(new HttpClient(handler, disposeHandler), true, configureRestClient, configureSerialization) { }
189209

190-
void ConfigureHttpClient() {
191-
if (Options.MaxTimeout > 0) HttpClient.Timeout = TimeSpan.FromMilliseconds(Options.MaxTimeout);
210+
static void ConfigureHttpClient(HttpClient httpClient, RestClientOptions options) {
211+
if (options.MaxTimeout > 0) httpClient.Timeout = TimeSpan.FromMilliseconds(options.MaxTimeout);
192212

193-
if (Options.UserAgent != null && HttpClient.DefaultRequestHeaders.UserAgent.All(x => x.Product?.Name != Options.UserAgent)) {
194-
HttpClient.DefaultRequestHeaders.TryAddWithoutValidation(KnownHeaders.UserAgent, Options.UserAgent);
213+
if (options.UserAgent != null && httpClient.DefaultRequestHeaders.UserAgent.All(x => x.Product?.Name != options.UserAgent)) {
214+
httpClient.DefaultRequestHeaders.TryAddWithoutValidation(KnownHeaders.UserAgent, options.UserAgent);
195215
}
196216

197-
if (Options.Expect100Continue != null) HttpClient.DefaultRequestHeaders.ExpectContinue = Options.Expect100Continue;
217+
if (options.Expect100Continue != null) httpClient.DefaultRequestHeaders.ExpectContinue = options.Expect100Continue;
198218
}
199219

200220
static void ConfigureHttpMessageHandler(HttpClientHandler handler, ReadOnlyRestClientOptions options) {

src/RestSharp/Serializers/RestSerializers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ internal RestResponse<T> Deserialize<T>(RestRequest request, RestResponse raw, R
9393
return factory?.GetSerializer().Deserializer;
9494

9595
ContentType? DetectContentType()
96-
=> response.Content[0] switch {
96+
=> response.Content![0] switch {
9797
'<' => ContentType.Xml,
9898
'{' or '[' => ContentType.Json,
9999
_ => null
Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
// Copyright (c) .NET Foundation and Contributors
2-
//
2+
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
55
// You may obtain a copy of the License at
6-
//
6+
//
77
// http://www.apache.org/licenses/LICENSE-2.0
8-
//
8+
//
99
// Unless required by applicable law or agreed to in writing, software
1010
// distributed under the License is distributed on an "AS IS" BASIS,
1111
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
14+
//
15+
16+
using System.Collections.Concurrent;
1417

1518
namespace RestSharp;
1619

17-
[PublicAPI]
18-
public class NameValuePair {
19-
public static NameValuePair Empty = new(null, null);
20+
static class SimpleClientFactory {
21+
static readonly ConcurrentDictionary<string, HttpClient> CachedClients = new();
2022

21-
public NameValuePair(string? name, string? value) {
22-
Name = name;
23-
Value = value;
23+
public static HttpClient GetClient(Uri baseUrl, Func<HttpClient> getClient) {
24+
var key = baseUrl.ToString();
25+
if (CachedClients.TryGetValue(key, out var client)) return client;
26+
client = getClient();
27+
CachedClients.TryAdd(key, client);
28+
return client;
2429
}
25-
26-
public string? Name { get; }
27-
public string? Value { get; }
28-
29-
public bool IsEmpty => Name == null;
30-
}
30+
}

0 commit comments

Comments
 (0)