Skip to content

Commit c486e78

Browse files
Conditional dispose of HttpClient (#1712)
* Dispose HttpClient conditionally * Replaced Twilio example with Twitter * Moved test files Serializer detection changes * Finalize the change in serializer detection * Add ignore case for serializer detection
1 parent c3c5ba1 commit c486e78

File tree

89 files changed

+353
-246
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+353
-246
lines changed

RestSharp.sln

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,23 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{9051DDA0
99
EndProject
1010
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Tests", "test\RestSharp.Tests\RestSharp.Tests.csproj", "{B1C55C9B-3287-4EB2-8ADD-795DBC77013D}"
1111
EndProject
12-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.IntegrationTests", "test\RestSharp.IntegrationTests\RestSharp.IntegrationTests.csproj", "{AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}"
12+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Tests.Integrated", "test\RestSharp.Tests.Integrated\RestSharp.Tests.Integrated.csproj", "{AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}"
1313
EndProject
1414
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Serializers.NewtonsoftJson", "src\RestSharp.Serializers.NewtonsoftJson\RestSharp.Serializers.NewtonsoftJson.csproj", "{4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}"
1515
EndProject
1616
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Serializers", "Serializers", "{8C7B43EB-2F93-483C-B433-E28F9386AD67}"
1717
EndProject
1818
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Tests.Shared", "test\RestSharp.Tests.Shared\RestSharp.Tests.Shared.csproj", "{73896669-F05C-41AC-9F6F-A11F549EDEDC}"
1919
EndProject
20-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Serializers.Json.Tests", "test\RestSharp.Serializers.Json.Tests\RestSharp.Serializers.Json.Tests.csproj", "{8BF81225-2F85-4412-AD18-6579CBA1879B}"
20+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Tests.Serializers.Json", "test\RestSharp.Tests.Serializers.Json\RestSharp.Tests.Serializers.Json.csproj", "{8BF81225-2F85-4412-AD18-6579CBA1879B}"
2121
EndProject
2222
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Perf", "Perf", "{1C42C435-8826-4044-8775-A1DA40EF4866}"
2323
EndProject
2424
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Benchmarks", "benchmarks\RestSharp.Benchmarks\RestSharp.Benchmarks.csproj", "{997AEFE5-D7D4-4033-A31A-07F476D6FE5D}"
2525
EndProject
2626
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.InteractiveTests", "test\RestSharp.InteractiveTests\RestSharp.InteractiveTests.csproj", "{6D7D1D60-4473-4C52-800C-9B892C6640A5}"
2727
EndProject
28-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Serializers.Xml.Tests", "test\RestSharp.Serializers.Xml.Tests\RestSharp.Serializers.Xml.Tests.csproj", "{E6D94C12-9AD7-46E6-AB62-3676F25FDE51}"
28+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Tests.Serializers.Xml", "test\RestSharp.Tests.Serializers.Xml\RestSharp.Tests.Serializers.Xml.csproj", "{E6D94C12-9AD7-46E6-AB62-3676F25FDE51}"
2929
EndProject
3030
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Serializers.Xml", "src\RestSharp.Serializers.Xml\RestSharp.Serializers.Xml.csproj", "{4A35B1C5-520D-4267-BA70-2DCEAC0A5662}"
3131
EndProject

docs/usage.md

Lines changed: 112 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -2,114 +2,142 @@
22
title: Usage
33
---
44

5-
## Recommended Usage
5+
## Recommended usage
66

7-
RestSharp works best as the foundation for a proxy class for your API. Here are a couple of examples from the <a href="http://github.com/twilio/twilio-csharp">Twilio</a> library.
7+
RestSharp works best as the foundation for a proxy class for your API. Each API would most probably require different settings for `RestClient`, so a dedicated API class (and its interface) gives you a nice isolation between different `RestClient` instances, and make them testable.
88

9-
Create a class to contain your API proxy implementation with an `ExecuteAsync` (or any of the extensions) method for funneling all requests to the API.
10-
This allows you to set commonly-used parameters and other settings (like authentication) shared across requests.
11-
Because an account ID and secret key are required for every request you are required to pass those two values when
12-
creating a new instance of the proxy.
9+
Essentially, RestSharp is a wrapper around `HttpClient` that allows you to do the following:
10+
- Add default parameters of any kind (not just headers) to the client, once
11+
- Add parameters of any kind to each request (query, URL segment, form, attachment, serialized body, header) in a straightforward way
12+
- Serialize the payload to JSON or XML if necessary
13+
- Set the correct content headers (content type, disposition, length, etc)
14+
- Handle the remote endpoint response
15+
- Deserialize the response from JSON or XML if necessary
1316

14-
::: warning
15-
Note that exceptions from `ExecuteAsync` are not thrown but are available in the `ErrorException` property.
16-
:::
17+
As an example, let's look at a simple Twitter API v2 client, which uses OAuth2 machine-to-machine authentication. For it to work, you would need to have access to Twitter Developers portal, an a project, and an approved application inside the project with OAuth2 enabled.
18+
19+
### Authenticator
20+
21+
Before we can make any call to the API itself, we need to get a bearer token. Twitter exposes an endpoint `https://api.twitter.com/oauth2/token`. As it follows the OAuth2 conventions, the code can be used to create an authenticator for some other vendors.
22+
23+
First, we need a model for deserializing the token endpoint response. OAuth2 uses snake case for property naming, so we need to decorate model properties with `JsonPropertyName` attribute:
1724

1825
```csharp
19-
// TwilioApi.cs
20-
public class TwilioApi {
21-
const string BaseUrl = "https://api.twilio.com/2008-08-01";
26+
record TokenResponse {
27+
[JsonPropertyName("token_type")]
28+
public string TokenType { get; init; }
29+
[JsonPropertyName("access_token")]
30+
public string AccessToken { get; init; }
31+
}
32+
```
2233

23-
readonly RestClient _client;
34+
Next, we create the authenticator itself. It needs the API key and API key secret for calling the token endpoint using basic HTTP authentication. In addition, we can extend the list of parameters with the base URL, so it can be converted to a more generic OAuth2 authenticator.
2435

25-
string _accountSid;
36+
The easiest way to create an authenticator is to inherit is from the `AuthanticatorBase` base class:
2637

27-
public TwilioApi(string accountSid, string secretKey) {
28-
_client = new RestClient(BaseUrl);
29-
_client.Authenticator = new HttpBasicAuthenticator(accountSid, secretKey);
30-
_client.AddDefaultParameter(
31-
"AccountSid", _accountSid, ParameterType.UrlSegment
32-
); // used on every request
33-
_accountSid = accountSid;
38+
```csharp
39+
public class TwitterAuthenticator : AuthenticatorBase {
40+
readonly string _baseUrl;
41+
readonly string _clientId;
42+
readonly string _clientSecret;
43+
44+
public TwitterAuthenticator(string baseUrl, string clientId, string clientSecret) : base("") {
45+
_baseUrl = baseUrl;
46+
_clientId = clientId;
47+
_clientSecret = clientSecret;
48+
}
49+
50+
protected override async ValueTask<Parameter> GetAuthenticationParameter(string accessToken) {
51+
var token = string.IsNullOrEmpty(Token) ? await GetToken() : Token;
52+
return new HeaderParameter(KnownHeaders.Authorization, token);
3453
}
3554
}
3655
```
3756

38-
Next, define a class that maps to the data returned by the API.
57+
During the first call made by the client using the authenticator, it will find out that the `Token` property is empty. It will then call the `GetToken` function to get the token once, and then will reuse the token going forwards.
58+
59+
Now, we need to include the `GetToken` function to the class:
3960

4061
```csharp
41-
// Call.cs
42-
public class Call {
43-
public string Sid { get; set; }
44-
public DateTime DateCreated { get; set; }
45-
public DateTime DateUpdated { get; set; }
46-
public string CallSegmentSid { get; set; }
47-
public string AccountSid { get; set; }
48-
public string Called { get; set; }
49-
public string Caller { get; set; }
50-
public string PhoneNumberSid { get; set; }
51-
public int Status { get; set; }
52-
public DateTime StartTime { get; set; }
53-
public DateTime EndTime { get; set; }
54-
public int Duration { get; set; }
55-
public decimal Price { get; set; }
56-
public int Flags { get; set; }
62+
async Task<string> GetToken() {
63+
var options = new RestClientOptions(_baseUrl);
64+
using var client = new RestClient(options) {
65+
Authenticator = new HttpBasicAuthenticator(_clientId, _clientSecret),
66+
};
67+
68+
var request = new RestRequest("oauth2/token")
69+
.AddParameter("grant_type", "client_credentials");
70+
var response = await client.PostAsync<TokenResponse>(request);
71+
return $"{response!.TokenType} {response!.AccessToken}";
5772
}
5873
```
5974

60-
Then add a method to query the API for the details of a specific Call resource.
75+
As we need to make a call to the token endpoint, we need our own, short-lived instance of `RestClient`. Unlike the actual Twitter client, it will use the `HttpBasicAuthenticator` to send API key and secret as username and password. The client is then gets disposed as we only use it once.
6176

62-
```csharp
63-
// TwilioApi.cs, GetCall method of TwilioApi class
64-
public Task<Call> GetCall(string callSid) {
65-
var request = new RestRequest("Accounts/{AccountSid}/Calls/{CallSid}");
66-
request.RootElement = "Call";
67-
request.AddParameter("CallSid", callSid, ParameterType.UrlSegment);
77+
Here we add a POST parameter `grant_type` with `client_credentials` as its value. At the moment, it's the only supported value.
78+
79+
The POST request will use the `application/x-www-form-urlencoded` content type by default.
80+
81+
### API client
6882

69-
return _client.GetAsync<Call>(request);
83+
Now, we can start creating the API client itself. Here we start with a single function that retrieves one Twitter user. Let's being by defining the API client interface:
84+
85+
```csharp
86+
public interface ITwitterClient {
87+
Task<TwitterUser> GetUser(string user);
7088
}
7189
```
7290

73-
There's some magic here that RestSharp takes care of, so you don't have to.
91+
As the function returns a `TwitterUser` instance, we need to define it as a model:
92+
93+
```csharp
94+
public record TwitterUser(string Id, string Name, string Username);
95+
```
7496

75-
The API returns XML, which is automatically detected and deserialized to the Call object using the default `XmlDeserializer`.
76-
By default, a call is made via a GET HTTP request. You can change this by setting the `Method` property of `RestRequest`
77-
or specifying the method in the constructor when creating an instance (covered below).
78-
Parameters of type `UrlSegment` have their values injected into the URL based on a matching token name existing in the Resource property value.
79-
`AccountSid` is set in `TwilioApi.Execute` because it is common to every request.
80-
We specify the name of the root element to start deserializing from. In this case, the XML returned is `<Response><Call>...</Call></Response>` and since the Response element itself does not contain any information relevant to our model, we start the deserializing one step down the tree.
97+
When that is done, we can implement the interface and add all the necessary code blocks to get a working API client.
8198

82-
You can also make POST (and PUT/DELETE/HEAD/OPTIONS) requests:
99+
The client class needs the following:
100+
- A constructor, which accepts API credentials to be passed to the authenticator
101+
- A wrapped `RestClient` instance with Twitter API base URI pre-configured
102+
- The `TwitterAuthenticator` that we created previously as the client authenticator
103+
- The actual function to get the user
83104

84105
```csharp
85-
// TwilioApi.cs, method of TwilioApi class
86-
public Task<Call> InitiateOutboundCall(CallOptions options) {
87-
var request = new RestRequest("Accounts/{AccountSid}/Calls") {
88-
RootElement = "Calls"
106+
public class TwitterClient : ITwitterClient, IDisposable {
107+
readonly RestClient _client;
108+
109+
public TwitterClient(string apiKey, string apiKeySecret) {
110+
var options = new RestClientOptions("https://api.twitter.com/2");
111+
112+
_client = new RestClient(options) {
113+
Authenticator = new TwitterAuthenticator("https://api.twitter.com", apiKey, apiKeySecret)
114+
};
89115
}
90-
.AddParameter("Caller", options.Caller)
91-
.AddParameter("Called", options.Called)
92-
.AddParameter("Url", options.Url);
93116

94-
if (options.Method.HasValue) request.AddParameter("Method", options.Method);
95-
if (options.SendDigits.HasValue()) request.AddParameter("SendDigits", options.SendDigits);
96-
if (options.IfMachine.HasValue) request.AddParameter("IfMachine", options.IfMachine.Value);
97-
if (options.Timeout.HasValue) request.AddParameter("Timeout", options.Timeout.Value);
117+
public async Task<TwitterUser> GetUser(string user) {
118+
var response = await _client.GetJsonAsync<TwitterSingleObject<TwitterUser>>(
119+
"users/by/username/{user}",
120+
new { user }
121+
);
122+
return response!.Data;
123+
}
98124

99-
return _client.PostAsync<Call>(request);
125+
record TwitterSingleObject<T>(T Data);
126+
127+
public void Dispose() {
128+
_client?.Dispose();
129+
GC.SuppressFinalize(this);
130+
}
100131
}
101132
```
102133

103-
This example also demonstrates RestSharp's lightweight validation helpers.
104-
These helpers allow you to verify before making the request that the values submitted are valid.
105-
Read more about Validation here.
134+
Couple of things that don't fall to the "basics" list.
135+
- The API client class needs to be disposable, so it can dispose the wrapped `HttpClient` instance
136+
- Twitter API returns wrapped models. In this case we use the `TwitterSingleObject` wrapper, in other methods you'd need a similar object with `T[] Data` to accept collections
106137

107-
All the values added via `AddParameter` in this example will be submitted as a standard encoded form,
108-
similar to a form submission made via a web page. If this were a GET-style request (GET/DELETE/OPTIONS/HEAD),
109-
the parameter values would be submitted via the query string instead. You can also add header and cookie
110-
parameters with `AddParameter`. To add all properties for an object as parameters, use `AddObject`.
111-
To add a file for upload, use `AddFile` (the request will be sent as a multipart encoded form).
112-
To include a request body like XML or JSON, use `AddXmlBody` or `AddJsonBody`.
138+
You can find the full example code in [this gist](https://gist.github.com/alexeyzimarev/62d77bb25d7aa5bb4b9685461f8aabdd).
139+
140+
Such a client can and should be used _as a singleton_, as it's thread-safe and authentication-aware. If you make it a transient dependency, you'll keep bombarding Twitter with token requests and effectively half your request limit.
113141

114142
## Request Parameters
115143

@@ -160,6 +188,15 @@ If you have `GetOrPost` parameters as well, they will overwrite the `RequestBody
160188

161189
We recommend using `AddJsonBody` or `AddXmlBody` methods instead of `AddParameter` with type `BodyParameter`. Those methods will set the proper request type and do the serialization work for you.
162190

191+
#### AddStringBody
192+
193+
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:
194+
195+
```csharp
196+
const json = "{ data: { foo: \"bar\" } }";
197+
request.AddStringBody(json, ContentType.Json);
198+
```
199+
163200
#### AddJsonBody
164201

165202
When you call `AddJsonBody`, it does the following for you:

src/RestSharp.Serializers.NewtonsoftJson/JsonNetSerializer.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,11 @@ public class JsonNetSerializer : IRestSerializer, ISerializer, IDeserializer {
7373
public ISerializer Serializer => this;
7474
public IDeserializer Deserializer => this;
7575

76-
public string[] SupportedContentTypes { get; } = {
77-
"application/json", "text/json", "text/x-json", "text/javascript", "*+json"
78-
};
76+
public string[] AcceptedContentTypes => Serializers.ContentType.JsonAccept;
7977

8078
public string ContentType { get; set; } = "application/json";
8179

80+
public SupportsContentType SupportsContentType => contentType => contentType.Contains("json");
81+
8282
public DataFormat DataFormat => DataFormat.Json;
8383
}

src/RestSharp/Properties/AssemblyInfo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[assembly:
44
InternalsVisibleTo(
5-
"RestSharp.IntegrationTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fda57af14a288d46e3efea89617037585c4de57159cd536ca6dff792ea1d6addc665f2fccb4285413d9d44db5a1be87cb82686db200d16325ed9c42c89cd4824d8cc447f7cee2ac000924c3bceeb1b7fcb5cc1a3901785964d48ce14172001084134f4dcd9973c3776713b595443b1064bb53e2eeb924969244d354e46495e9d"
5+
"RestSharp.Tests.Integrated, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fda57af14a288d46e3efea89617037585c4de57159cd536ca6dff792ea1d6addc665f2fccb4285413d9d44db5a1be87cb82686db200d16325ed9c42c89cd4824d8cc447f7cee2ac000924c3bceeb1b7fcb5cc1a3901785964d48ce14172001084134f4dcd9973c3776713b595443b1064bb53e2eeb924969244d354e46495e9d"
66
),
77
InternalsVisibleTo(
88
"RestSharp.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fda57af14a288d46e3efea89617037585c4de57159cd536ca6dff792ea1d6addc665f2fccb4285413d9d44db5a1be87cb82686db200d16325ed9c42c89cd4824d8cc447f7cee2ac000924c3bceeb1b7fcb5cc1a3901785964d48ce14172001084134f4dcd9973c3776713b595443b1064bb53e2eeb924969244d354e46495e9d"

src/RestSharp/Request/HttpContentExtensions.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,30 @@
1313
// limitations under the License.
1414
//
1515

16-
namespace RestSharp;
16+
using System.Linq.Expressions;
17+
18+
namespace RestSharp;
1719

1820
public static class HttpContentExtensions {
19-
public static string GetFormBoundary(this HttpContent content) {
21+
static readonly Func<MultipartContent, string> GetBoundary = GetFieldAccessor<MultipartContent, string>("_boundary");
22+
23+
public static string GetFormBoundary(this MultipartFormDataContent content) {
24+
return GetBoundary(content);
2025
var contentType = content.Headers.ContentType?.ToString();
2126
var index = contentType?.IndexOf("boundary=", StringComparison.Ordinal) ?? 0;
2227
return index > 0 ? GetFormBoundary(contentType!, index) : "";
23-
}
24-
28+
}
29+
2530
static string GetFormBoundary(string headerValue, int index) {
2631
var part = headerValue.Substring(index);
2732
return part.Substring(10, 36);
2833
}
34+
35+
static Func<T, TReturn> GetFieldAccessor<T, TReturn>(string fieldName) {
36+
var param = Expression.Parameter(typeof(T), "arg");
37+
var member = Expression.Field(param, fieldName);
38+
var lambda = Expression.Lambda(typeof(Func<T, TReturn>), member, param);
39+
var compiled = (Func<T, TReturn>)lambda.Compile();
40+
return compiled;
41+
}
2942
}

src/RestSharp/Request/HttpRequestMessageExtensions.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414
//
1515

16+
using System.Net.Http.Headers;
1617
using RestSharp.Extensions;
1718

1819
namespace RestSharp;
@@ -21,13 +22,13 @@ static class HttpRequestMessageExtensions {
2122
public static void AddHeaders(this HttpRequestMessage message, RequestHeaders headers) {
2223
var headerParameters = headers.Parameters.Where(x => !RequestContent.ContentHeaders.Contains(x.Name));
2324

24-
headerParameters.ForEach(AddHeader);
25+
headerParameters.ForEach(x => AddHeader(x, message.Headers));
2526

26-
void AddHeader(Parameter parameter) {
27+
void AddHeader(Parameter parameter, HttpHeaders httpHeaders) {
2728
var parameterStringValue = parameter.Value!.ToString();
2829

29-
message.Headers.Remove(parameter.Name!);
30-
message.Headers.TryAddWithoutValidation(parameter.Name!, parameterStringValue);
30+
httpHeaders.Remove(parameter.Name!);
31+
httpHeaders.TryAddWithoutValidation(parameter.Name!, parameterStringValue);
3132
}
3233
}
3334
}

src/RestSharp/Request/RequestContent.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,10 @@ void AddHeader(Parameter parameter) {
185185
}
186186
}
187187

188-
string GetContentTypeHeader(string contentType) {
189-
var boundary = Content!.GetFormBoundary();
190-
return boundary.IsEmpty() ? contentType : $"{contentType}; boundary=\"{boundary}\"";
191-
}
188+
string GetContentTypeHeader(string contentType)
189+
=> Content is MultipartFormDataContent mpContent
190+
? $"{contentType}; boundary=\"{mpContent.GetFormBoundary()}\""
191+
: contentType;
192192

193193
public void Dispose() {
194194
_streams.ForEach(x => x.Dispose());

0 commit comments

Comments
 (0)