Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions RestSharp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,23 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{9051DDA0
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Tests", "test\RestSharp.Tests\RestSharp.Tests.csproj", "{B1C55C9B-3287-4EB2-8ADD-795DBC77013D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.IntegrationTests", "test\RestSharp.IntegrationTests\RestSharp.IntegrationTests.csproj", "{AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Tests.Integrated", "test\RestSharp.Tests.Integrated\RestSharp.Tests.Integrated.csproj", "{AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Serializers.NewtonsoftJson", "src\RestSharp.Serializers.NewtonsoftJson\RestSharp.Serializers.NewtonsoftJson.csproj", "{4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Serializers", "Serializers", "{8C7B43EB-2F93-483C-B433-E28F9386AD67}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Tests.Shared", "test\RestSharp.Tests.Shared\RestSharp.Tests.Shared.csproj", "{73896669-F05C-41AC-9F6F-A11F549EDEDC}"
EndProject
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}"
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}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Perf", "Perf", "{1C42C435-8826-4044-8775-A1DA40EF4866}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Benchmarks", "benchmarks\RestSharp.Benchmarks\RestSharp.Benchmarks.csproj", "{997AEFE5-D7D4-4033-A31A-07F476D6FE5D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.InteractiveTests", "test\RestSharp.InteractiveTests\RestSharp.InteractiveTests.csproj", "{6D7D1D60-4473-4C52-800C-9B892C6640A5}"
EndProject
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}"
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}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Serializers.Xml", "src\RestSharp.Serializers.Xml\RestSharp.Serializers.Xml.csproj", "{4A35B1C5-520D-4267-BA70-2DCEAC0A5662}"
EndProject
Expand Down
187 changes: 112 additions & 75 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,114 +2,142 @@
title: Usage
---

## Recommended Usage
## Recommended usage

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.
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.

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.
This allows you to set commonly-used parameters and other settings (like authentication) shared across requests.
Because an account ID and secret key are required for every request you are required to pass those two values when
creating a new instance of the proxy.
Essentially, RestSharp is a wrapper around `HttpClient` that allows you to do the following:
- Add default parameters of any kind (not just headers) to the client, once
- Add parameters of any kind to each request (query, URL segment, form, attachment, serialized body, header) in a straightforward way
- Serialize the payload to JSON or XML if necessary
- Set the correct content headers (content type, disposition, length, etc)
- Handle the remote endpoint response
- Deserialize the response from JSON or XML if necessary

::: warning
Note that exceptions from `ExecuteAsync` are not thrown but are available in the `ErrorException` property.
:::
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.

### Authenticator

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.

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:

```csharp
// TwilioApi.cs
public class TwilioApi {
const string BaseUrl = "https://api.twilio.com/2008-08-01";
record TokenResponse {
[JsonPropertyName("token_type")]
public string TokenType { get; init; }
[JsonPropertyName("access_token")]
public string AccessToken { get; init; }
}
```

readonly RestClient _client;
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.

string _accountSid;
The easiest way to create an authenticator is to inherit is from the `AuthanticatorBase` base class:

public TwilioApi(string accountSid, string secretKey) {
_client = new RestClient(BaseUrl);
_client.Authenticator = new HttpBasicAuthenticator(accountSid, secretKey);
_client.AddDefaultParameter(
"AccountSid", _accountSid, ParameterType.UrlSegment
); // used on every request
_accountSid = accountSid;
```csharp
public class TwitterAuthenticator : AuthenticatorBase {
readonly string _baseUrl;
readonly string _clientId;
readonly string _clientSecret;

public TwitterAuthenticator(string baseUrl, string clientId, string clientSecret) : base("") {
_baseUrl = baseUrl;
_clientId = clientId;
_clientSecret = clientSecret;
}

protected override async ValueTask<Parameter> GetAuthenticationParameter(string accessToken) {
var token = string.IsNullOrEmpty(Token) ? await GetToken() : Token;
return new HeaderParameter(KnownHeaders.Authorization, token);
}
}
```

Next, define a class that maps to the data returned by the API.
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.

Now, we need to include the `GetToken` function to the class:

```csharp
// Call.cs
public class Call {
public string Sid { get; set; }
public DateTime DateCreated { get; set; }
public DateTime DateUpdated { get; set; }
public string CallSegmentSid { get; set; }
public string AccountSid { get; set; }
public string Called { get; set; }
public string Caller { get; set; }
public string PhoneNumberSid { get; set; }
public int Status { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public int Duration { get; set; }
public decimal Price { get; set; }
public int Flags { get; set; }
async Task<string> GetToken() {
var options = new RestClientOptions(_baseUrl);
using var client = new RestClient(options) {
Authenticator = new HttpBasicAuthenticator(_clientId, _clientSecret),
};

var request = new RestRequest("oauth2/token")
.AddParameter("grant_type", "client_credentials");
var response = await client.PostAsync<TokenResponse>(request);
return $"{response!.TokenType} {response!.AccessToken}";
}
```

Then add a method to query the API for the details of a specific Call resource.
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.

```csharp
// TwilioApi.cs, GetCall method of TwilioApi class
public Task<Call> GetCall(string callSid) {
var request = new RestRequest("Accounts/{AccountSid}/Calls/{CallSid}");
request.RootElement = "Call";
request.AddParameter("CallSid", callSid, ParameterType.UrlSegment);
Here we add a POST parameter `grant_type` with `client_credentials` as its value. At the moment, it's the only supported value.

The POST request will use the `application/x-www-form-urlencoded` content type by default.

### API client

return _client.GetAsync<Call>(request);
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:

```csharp
public interface ITwitterClient {
Task<TwitterUser> GetUser(string user);
}
```

There's some magic here that RestSharp takes care of, so you don't have to.
As the function returns a `TwitterUser` instance, we need to define it as a model:

```csharp
public record TwitterUser(string Id, string Name, string Username);
```

The API returns XML, which is automatically detected and deserialized to the Call object using the default `XmlDeserializer`.
By default, a call is made via a GET HTTP request. You can change this by setting the `Method` property of `RestRequest`
or specifying the method in the constructor when creating an instance (covered below).
Parameters of type `UrlSegment` have their values injected into the URL based on a matching token name existing in the Resource property value.
`AccountSid` is set in `TwilioApi.Execute` because it is common to every request.
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.
When that is done, we can implement the interface and add all the necessary code blocks to get a working API client.

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

```csharp
// TwilioApi.cs, method of TwilioApi class
public Task<Call> InitiateOutboundCall(CallOptions options) {
var request = new RestRequest("Accounts/{AccountSid}/Calls") {
RootElement = "Calls"
public class TwitterClient : ITwitterClient, IDisposable {
readonly RestClient _client;

public TwitterClient(string apiKey, string apiKeySecret) {
var options = new RestClientOptions("https://api.twitter.com/2");

_client = new RestClient(options) {
Authenticator = new TwitterAuthenticator("https://api.twitter.com", apiKey, apiKeySecret)
};
}
.AddParameter("Caller", options.Caller)
.AddParameter("Called", options.Called)
.AddParameter("Url", options.Url);

if (options.Method.HasValue) request.AddParameter("Method", options.Method);
if (options.SendDigits.HasValue()) request.AddParameter("SendDigits", options.SendDigits);
if (options.IfMachine.HasValue) request.AddParameter("IfMachine", options.IfMachine.Value);
if (options.Timeout.HasValue) request.AddParameter("Timeout", options.Timeout.Value);
public async Task<TwitterUser> GetUser(string user) {
var response = await _client.GetJsonAsync<TwitterSingleObject<TwitterUser>>(
"users/by/username/{user}",
new { user }
);
return response!.Data;
}

return _client.PostAsync<Call>(request);
record TwitterSingleObject<T>(T Data);

public void Dispose() {
_client?.Dispose();
GC.SuppressFinalize(this);
}
}
```

This example also demonstrates RestSharp's lightweight validation helpers.
These helpers allow you to verify before making the request that the values submitted are valid.
Read more about Validation here.
Couple of things that don't fall to the "basics" list.
- The API client class needs to be disposable, so it can dispose the wrapped `HttpClient` instance
- 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

All the values added via `AddParameter` in this example will be submitted as a standard encoded form,
similar to a form submission made via a web page. If this were a GET-style request (GET/DELETE/OPTIONS/HEAD),
the parameter values would be submitted via the query string instead. You can also add header and cookie
parameters with `AddParameter`. To add all properties for an object as parameters, use `AddObject`.
To add a file for upload, use `AddFile` (the request will be sent as a multipart encoded form).
To include a request body like XML or JSON, use `AddXmlBody` or `AddJsonBody`.
You can find the full example code in [this gist](https://gist.github.com/alexeyzimarev/62d77bb25d7aa5bb4b9685461f8aabdd).

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.

## Request Parameters

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

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.

#### 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:

```csharp
const json = "{ data: { foo: \"bar\" } }";
request.AddStringBody(json, ContentType.Json);
```

#### AddJsonBody

When you call `AddJsonBody`, it does the following for you:
Expand Down
6 changes: 3 additions & 3 deletions src/RestSharp.Serializers.NewtonsoftJson/JsonNetSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,11 @@ public class JsonNetSerializer : IRestSerializer, ISerializer, IDeserializer {
public ISerializer Serializer => this;
public IDeserializer Deserializer => this;

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

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

public SupportsContentType SupportsContentType => contentType => contentType.Contains("json");

public DataFormat DataFormat => DataFormat.Json;
}
2 changes: 1 addition & 1 deletion src/RestSharp/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[assembly:
InternalsVisibleTo(
"RestSharp.IntegrationTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fda57af14a288d46e3efea89617037585c4de57159cd536ca6dff792ea1d6addc665f2fccb4285413d9d44db5a1be87cb82686db200d16325ed9c42c89cd4824d8cc447f7cee2ac000924c3bceeb1b7fcb5cc1a3901785964d48ce14172001084134f4dcd9973c3776713b595443b1064bb53e2eeb924969244d354e46495e9d"
"RestSharp.Tests.Integrated, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fda57af14a288d46e3efea89617037585c4de57159cd536ca6dff792ea1d6addc665f2fccb4285413d9d44db5a1be87cb82686db200d16325ed9c42c89cd4824d8cc447f7cee2ac000924c3bceeb1b7fcb5cc1a3901785964d48ce14172001084134f4dcd9973c3776713b595443b1064bb53e2eeb924969244d354e46495e9d"
),
InternalsVisibleTo(
"RestSharp.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fda57af14a288d46e3efea89617037585c4de57159cd536ca6dff792ea1d6addc665f2fccb4285413d9d44db5a1be87cb82686db200d16325ed9c42c89cd4824d8cc447f7cee2ac000924c3bceeb1b7fcb5cc1a3901785964d48ce14172001084134f4dcd9973c3776713b595443b1064bb53e2eeb924969244d354e46495e9d"
Expand Down
21 changes: 17 additions & 4 deletions src/RestSharp/Request/HttpContentExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,30 @@
// limitations under the License.
//

namespace RestSharp;
using System.Linq.Expressions;

namespace RestSharp;

public static class HttpContentExtensions {
public static string GetFormBoundary(this HttpContent content) {
static readonly Func<MultipartContent, string> GetBoundary = GetFieldAccessor<MultipartContent, string>("_boundary");

public static string GetFormBoundary(this MultipartFormDataContent content) {
return GetBoundary(content);
var contentType = content.Headers.ContentType?.ToString();
var index = contentType?.IndexOf("boundary=", StringComparison.Ordinal) ?? 0;
return index > 0 ? GetFormBoundary(contentType!, index) : "";
}
}

static string GetFormBoundary(string headerValue, int index) {
var part = headerValue.Substring(index);
return part.Substring(10, 36);
}

static Func<T, TReturn> GetFieldAccessor<T, TReturn>(string fieldName) {
var param = Expression.Parameter(typeof(T), "arg");
var member = Expression.Field(param, fieldName);
var lambda = Expression.Lambda(typeof(Func<T, TReturn>), member, param);
var compiled = (Func<T, TReturn>)lambda.Compile();
return compiled;
}
}
9 changes: 5 additions & 4 deletions src/RestSharp/Request/HttpRequestMessageExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.
//

using System.Net.Http.Headers;
using RestSharp.Extensions;

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

headerParameters.ForEach(AddHeader);
headerParameters.ForEach(x => AddHeader(x, message.Headers));

void AddHeader(Parameter parameter) {
void AddHeader(Parameter parameter, HttpHeaders httpHeaders) {
var parameterStringValue = parameter.Value!.ToString();

message.Headers.Remove(parameter.Name!);
message.Headers.TryAddWithoutValidation(parameter.Name!, parameterStringValue);
httpHeaders.Remove(parameter.Name!);
httpHeaders.TryAddWithoutValidation(parameter.Name!, parameterStringValue);
}
}
}
8 changes: 4 additions & 4 deletions src/RestSharp/Request/RequestContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,10 @@ void AddHeader(Parameter parameter) {
}
}

string GetContentTypeHeader(string contentType) {
var boundary = Content!.GetFormBoundary();
return boundary.IsEmpty() ? contentType : $"{contentType}; boundary=\"{boundary}\"";
}
string GetContentTypeHeader(string contentType)
=> Content is MultipartFormDataContent mpContent
? $"{contentType}; boundary=\"{mpContent.GetFormBoundary()}\""
: contentType;

public void Dispose() {
_streams.ForEach(x => x.Dispose());
Expand Down
Loading