using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Formatting; using System.Net.Http.Headers; using System.Reflection; using System.Text; using System.Threading.Tasks; using Pathoschild.Http.Client.Extensibility; using Pathoschild.Http.Client.Internal; using Pathoschild.Http.Client.Retry; namespace Pathoschild.Http.Client { /// Provides convenience methods for configuring the HTTP client. public static class FluentClientExtensions { /********* ** Public methods *********/ /**** ** IClient ****/ /// Remove all HTTP filters of the specified type. /// The filter type. /// The filters to adjust. /// Returns whether a filter was removed. public static bool Remove(this ICollection filters) where TFilter : IHttpFilter { TFilter[] remove = filters.OfType().ToArray(); foreach (TFilter filter in remove) filters.Remove(filter); return remove.Any(); } /// Create an asynchronous HTTP DELETE request message (but don't dispatch it yet). /// The client. /// The URI to send the request to. /// Returns a request builder. /// The instance has been disposed. public static IRequest DeleteAsync(this IClient client, string resource) { return client.SendAsync(HttpMethod.Delete, resource); } /// Create an asynchronous HTTP GET request message (but don't dispatch it yet). /// The client. /// The URI to send the request to. /// Returns a request builder. /// The instance has been disposed. public static IRequest GetAsync(this IClient client, string resource) { return client.SendAsync(HttpMethod.Get, resource); } /// Create an asynchronous HTTP POST request message (but don't dispatch it yet). /// The client. /// The URI to send the request to. /// Returns a request builder. /// The instance has been disposed. public static IRequest PostAsync(this IClient client, string resource) { return client.SendAsync(HttpMethod.Post, resource); } /// Create an asynchronous HTTP POST request message (but don't dispatch it yet). /// The client. /// The request body type. /// The URI to send the request to. /// The request body. /// Returns a request builder. /// The instance has been disposed. public static IRequest PostAsync(this IClient client, string resource, TBody body) { return client.PostAsync(resource).WithBody(body); } /// Create an asynchronous HTTP PUT request message (but don't dispatch it yet). /// The client. /// The URI to send the request to. /// Returns a request builder. /// The instance has been disposed. public static IRequest PutAsync(this IClient client, string resource) { return client.SendAsync(HttpMethod.Put, resource); } /// Create an asynchronous HTTP PUT request message (but don't dispatch it yet). /// The client. /// The request body type. /// The URI to send the request to. /// The request body. /// Returns a request builder. /// The instance has been disposed. public static IRequest PutAsync(this IClient client, string resource, TBody body) { return client.PutAsync(resource).WithBody(body); } /// Create an asynchronous HTTP PATCH request message (but don't dispatch it yet). /// The client. /// The URI to send the request to. /// Returns a request builder. /// The instance has been disposed. public static IRequest PatchAsync(this IClient client, string resource) { return client.SendAsync(new HttpMethod("PATCH"), resource); } /// Create an asynchronous HTTP PATCH request message (but don't dispatch it yet). /// The client. /// The request body type. /// The URI to send the request to. /// The request body. /// Returns a request builder. /// The instance has been disposed. public static IRequest PatchAsync(this IClient client, string resource, TBody body) { return client.PatchAsync(resource).WithBody(body); } /// Create an asynchronous HTTP request message (but don't dispatch it yet). /// The client. /// The HTTP method. /// The URI to send the request to. /// Returns a request builder. /// The instance has been disposed. public static IRequest SendAsync(this IClient client, HttpMethod method, string resource) { var uri = FluentClientExtensions.ResolveFinalUrl(client.BaseClient.BaseAddress, resource); var message = Factory.GetRequestMessage(method, uri, client.Formatters); return client.SendAsync(message); } /// Set the default authentication header using basic auth. /// The client. /// The username. /// The password. public static IClient SetBasicAuthentication(this IClient client, string username, string password) { return client.SetAuthentication("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Concat(username, ":", password)))); } /// Set the default authentication header using a bearer token. /// The client. /// The bearer token (typically an API key). public static IClient SetBearerAuthentication(this IClient client, string token) { return client.SetAuthentication("Bearer", token); } /// Set the default request coordinator. /// The client. /// A method which returns whether a request should be retried. /// The intervals between each retry attempt. public static IClient SetRequestCoordinator(this IClient client, Func shouldRetry, params TimeSpan[] intervals) { return client.SetRequestCoordinator(new RetryCoordinator(shouldRetry, intervals)); } /// Set the default request coordinator. /// The client. /// The maximum number of times to retry a request before failing. /// A method which returns whether a request should be retried. /// A method which returns the time to wait until the next retry. This is passed the retry index (starting at 1) and the last HTTP response received. public static IClient SetRequestCoordinator(this IClient client, int maxRetries, Func shouldRetry, Func getDelay) { return client.SetRequestCoordinator(new RetryCoordinator(maxRetries, shouldRetry, getDelay)); } /// Set the default request coordinator. /// The client. /// The retry configuration (or null for the default coordinator). public static IClient SetRequestCoordinator(this IClient client, IRetryConfig config) { return client.SetRequestCoordinator(new RetryCoordinator(config)); } /// Set default options for all requests. /// The client. /// Whether to ignore null arguments when the request is dispatched (or null to leave the option unchanged). /// Whether HTTP error responses like HTTP 404 should be ignored; else raised as exceptions (or null to leave the option unchanged). public static IClient SetOptions(this IClient client, bool? ignoreHttpErrors = null, bool? ignoreNullArguments = null) { return client.SetOptions(new FluentClientOptions { IgnoreHttpErrors = ignoreHttpErrors, IgnoreNullArguments = ignoreNullArguments }); } /**** ** IRequest ****/ /// Add an authentication header using basic auth. /// The request. /// The username. /// The password. public static IRequest WithBasicAuthentication(this IRequest request, string username, string password) { return request.WithAuthentication("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Concat(username, ":", password)))); } /// Add an authentication header using a bearer token. /// The request. /// The bearer token (typically an API key). public static IRequest WithBearerAuthentication(this IRequest request, string token) { return request.WithAuthentication("Bearer", token); } /// Set the body content of the HTTP request. /// The request. /// The model to serialize into the HTTP body content, or an HttpContent instance. /// Returns the request builder for chaining. /// No MediaTypeFormatters are available on the API client for this content type. public static IRequest WithBody(this IRequest request, T body) { // HttpContent if (typeof(HttpContent).GetTypeInfo().IsAssignableFrom(typeof(T).GetTypeInfo())) return request.WithBody(p => (HttpContent)(object)body); // model return request.WithBody(p => p.Model(body)); } /// Set the body content of the HTTP request. /// The request. /// The model to serialize into the HTTP body content. /// The request body format (or null to use the first supported Content-Type in the ). /// Returns the request builder for chaining. /// No MediaTypeFormatters are available on the API client for this content type. [Obsolete("Will be removed in 4.0. Use `" + nameof(WithBody) + "` with a builder instead.")] public static IRequest WithBody(this IRequest request, T body, MediaTypeHeaderValue contentType = null) { return request.WithBody(p => p.Model(body, contentType)); } /// Set the body content of the HTTP request. /// The request. /// The model to serialize into the HTTP body content. /// The media type formatter with which to format the request body format. /// The HTTP media type (or null for the 's default). /// Returns the request builder for chaining. [Obsolete("Will be removed in 4.0. Use `" + nameof(WithBody) + "` with a builder instead.")] public static IRequest WithBody(this IRequest request, T body, MediaTypeFormatter formatter, string mediaType = null) { return request.WithBody(p => p.Model(body, formatter, mediaType)); } /// Set the request coordinator for this request /// The request. /// A lambda which returns whether a request should be retried. /// The intervals between each retry attempt. public static IRequest WithRequestCoordinator(this IRequest request, Func shouldRetry, params TimeSpan[] intervals) { return request.WithRequestCoordinator(new RetryCoordinator(shouldRetry, intervals)); } /// Set the request coordinator for this request /// The request. /// The maximum number of times to retry a request before failing. /// A method which returns whether a request should be retried. /// A method which returns the time to wait until the next retry. This is passed the retry index (starting at 1) and the last HTTP response received. public static IRequest WithRequestCoordinator(this IRequest request, int maxRetries, Func shouldRetry, Func getDelay) { return request.WithRequestCoordinator(new RetryCoordinator(maxRetries, shouldRetry, getDelay)); } /// Set the request coordinator for this request /// The request. /// The retry config (or null to use the default behaviour). public static IRequest WithRequestCoordinator(this IRequest request, IRetryConfig config) { return request.WithRequestCoordinator(new RetryCoordinator(config)); } /// Set options for this request. /// The request. /// Whether to ignore null arguments when the request is dispatched (or null to leave the option unchanged). /// Whether HTTP error responses like HTTP 404 should be ignored; else raised as exceptions (or null to leave the option unchanged). public static IRequest WithOptions(this IRequest request, bool? ignoreHttpErrors = null, bool? ignoreNullArguments = null) { return request.WithOptions(new RequestOptions { IgnoreHttpErrors = ignoreHttpErrors, IgnoreNullArguments = ignoreNullArguments }); } /********* ** Internal methods *********/ /// Get a copy of the request. /// The request to copy. /// Note that cloning a request isn't possible after it's dispatched, because the content stream is automatically disposed after the request. internal static async Task CloneAsync(this HttpRequestMessage request) { HttpRequestMessage clone = new HttpRequestMessage(request.Method, request.RequestUri) { Content = await request.Content.CloneAsync().ConfigureAwait(false), Version = request.Version }; foreach (var prop in request.Properties) clone.Properties.Add(prop); foreach (var header in request.Headers) clone.Headers.TryAddWithoutValidation(header.Key, header.Value); return clone; } /// Get a copy of the request content. /// The content to copy. /// Note that cloning content isn't possible after it's dispatched, because the stream is automatically disposed after the request. internal static async Task CloneAsync(this HttpContent content) { if (content == null) return null; Stream stream = new MemoryStream(); await content.CopyToAsync(stream).ConfigureAwait(false); stream.Position = 0; StreamContent clone = new StreamContent(stream); foreach (var header in content.Headers) clone.Headers.Add(header.Key, header.Value); return clone; } /// Resolve the final URL for a request. /// The base URL. /// The requested resource. private static Uri ResolveFinalUrl(Uri baseUrl, string resource) { // ignore if empty or already absolute if (string.IsNullOrWhiteSpace(resource)) return baseUrl; if (Uri.TryCreate(resource, UriKind.Absolute, out Uri absoluteUrl)) return absoluteUrl; // parse URLs resource = resource.Trim(); UriBuilder builder = new UriBuilder(baseUrl); // special case: combine if either side is a fragment if (!string.IsNullOrWhiteSpace(builder.Fragment) || resource.StartsWith("#")) return new Uri(baseUrl + resource); // special case: if resource is a query string, validate and append it if (resource.StartsWith("?") || resource.StartsWith("&")) { bool baseHasQuery = !string.IsNullOrWhiteSpace(builder.Query); if (baseHasQuery && resource.StartsWith("?")) throw new FormatException($"Can't add resource name '{resource}' to base URL '{baseUrl}' because the latter already has a query string."); if (!baseHasQuery && resource.StartsWith("&")) throw new FormatException($"Can't add resource name '{resource}' to base URL '{baseUrl}' because the latter doesn't have a query string."); return new Uri(baseUrl + resource); } // else make absolute URL if (!builder.Path.EndsWith("/")) { builder.Path += "/"; baseUrl = builder.Uri; } return new Uri(baseUrl, resource); } } }