mirror of
https://github.com/alexhopeoconnor/binarylane-dotnet.git
synced 2026-10-21 11:12:35 +10:00
Initial BinaryLane v2 API client
This commit is contained in:
@@ -0,0 +1,492 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BinaryLane.Api.V2.Authentication;
|
||||
using BinaryLane.Api.V2.Errors;
|
||||
|
||||
namespace BinaryLane.Api.V2.Http;
|
||||
|
||||
/// <summary>Default HTTP implementation for the BinaryLane API.</summary>
|
||||
public sealed class BinaryLaneHttpExecutor : IBinaryLaneApiExecutor
|
||||
{
|
||||
private const int MaximumDiagnosticBodyLength = 32 * 1024;
|
||||
private const long MaximumSuccessBodyLength = 16L * 1024 * 1024;
|
||||
private static readonly Uri DefaultBaseUri = new("https://api.binarylane.com.au/");
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IBinaryLaneTokenProvider _tokenProvider;
|
||||
private readonly BinaryLaneJsonSerializerOptions _json;
|
||||
|
||||
/// <summary>Initializes the executor.</summary>
|
||||
public BinaryLaneHttpExecutor(
|
||||
HttpClient httpClient,
|
||||
IBinaryLaneTokenProvider tokenProvider,
|
||||
BinaryLaneJsonSerializerOptions? json = null)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_tokenProvider = tokenProvider ?? throw new ArgumentNullException(nameof(tokenProvider));
|
||||
_json = json ?? new BinaryLaneJsonSerializerOptions();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BinaryLaneResponse<TResponse>> SendAsync<TResponse>(
|
||||
BinaryLaneRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
#else
|
||||
if (request is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
#endif
|
||||
|
||||
using var message = await CreateMessageAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
using var response = await _httpClient
|
||||
.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var headers = CollectHeaders(response);
|
||||
var body = response.Content is null
|
||||
? string.Empty
|
||||
: response.IsSuccessStatusCode
|
||||
? await ReadBodyAsync(response.Content, cancellationToken).ConfigureAwait(false)
|
||||
: await ReadDiagnosticBodyAsync(response.Content, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw CreateException(response.StatusCode, message.RequestUri!, body, headers);
|
||||
}
|
||||
|
||||
var result = Deserialize<TResponse>(body);
|
||||
return new BinaryLaneResponse<TResponse>(result, response.StatusCode, message.RequestUri!, headers);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BinaryLaneResponse<object?>> SendAsync(
|
||||
BinaryLaneRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
#else
|
||||
if (request is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
#endif
|
||||
|
||||
using var message = await CreateMessageAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
using var response = await _httpClient
|
||||
.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var headers = CollectHeaders(response);
|
||||
var body = response.Content is null
|
||||
? string.Empty
|
||||
: response.IsSuccessStatusCode
|
||||
? await ReadBodyAsync(response.Content, cancellationToken).ConfigureAwait(false)
|
||||
: await ReadDiagnosticBodyAsync(response.Content, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw CreateException(response.StatusCode, message.RequestUri!, body, headers);
|
||||
}
|
||||
|
||||
return new BinaryLaneResponse<object?>(null, response.StatusCode, message.RequestUri!, headers);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TResponse> GetAsync<TResponse>(
|
||||
string path,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
(await SendAsync<TResponse>(new BinaryLaneRequest(HttpMethod.Get, path) { Query = query }, cancellationToken)
|
||||
.ConfigureAwait(false)).Body;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TResponse> PostAsync<TResponse>(
|
||||
string path,
|
||||
object? body = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
(await SendAsync<TResponse>(new BinaryLaneRequest(HttpMethod.Post, path) { Body = body, Query = query }, cancellationToken)
|
||||
.ConfigureAwait(false)).Body;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TResponse> PutAsync<TResponse>(
|
||||
string path,
|
||||
object? body = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
(await SendAsync<TResponse>(new BinaryLaneRequest(HttpMethod.Put, path) { Body = body, Query = query }, cancellationToken)
|
||||
.ConfigureAwait(false)).Body;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TResponse> PatchAsync<TResponse>(
|
||||
string path,
|
||||
object? body = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
(await SendAsync<TResponse>(new BinaryLaneRequest(new HttpMethod("PATCH"), path) { Body = body, Query = query }, cancellationToken)
|
||||
.ConfigureAwait(false)).Body;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteAsync(
|
||||
string path,
|
||||
object? body = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await SendAsync(new BinaryLaneRequest(HttpMethod.Delete, path) { Body = body, Query = query }, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<HttpRequestMessage> CreateMessageAsync(BinaryLaneRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var requestUri = CreateUri(request.Path, request.Query);
|
||||
var token = await _tokenProvider.GetTokenAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
throw new InvalidOperationException("The configured BinaryLane token provider returned an empty token.");
|
||||
}
|
||||
|
||||
var message = new HttpRequestMessage(request.Method, requestUri);
|
||||
try
|
||||
{
|
||||
if (request.Headers is not null)
|
||||
{
|
||||
foreach (var header in request.Headers)
|
||||
{
|
||||
if (string.Equals(header.Key, "Authorization", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(header.Key, "Host", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"The '{header.Key}' header is managed by the BinaryLane client and cannot be overridden.",
|
||||
nameof(request));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
message.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
catch (Exception exception) when (exception is FormatException or InvalidOperationException)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"The '{header.Key}' header is not a valid request header for BinaryLane API requests.",
|
||||
nameof(request),
|
||||
exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
message.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
if (request.Body is not null)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(request.Body, _json.SerializerOptions);
|
||||
message.Content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
catch
|
||||
{
|
||||
message.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private Uri CreateUri(string path, IReadOnlyDictionary<string, object?>? query)
|
||||
{
|
||||
var baseUri = _httpClient.BaseAddress ?? DefaultBaseUri;
|
||||
if (!baseUri.IsAbsoluteUri ||
|
||||
baseUri.Scheme != Uri.UriSchemeHttps ||
|
||||
!string.IsNullOrEmpty(baseUri.UserInfo) ||
|
||||
!string.IsNullOrEmpty(baseUri.Query) ||
|
||||
!string.IsNullOrEmpty(baseUri.Fragment))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"HttpClient.BaseAddress must be an absolute HTTPS URL without credentials, a query string, or a fragment.");
|
||||
}
|
||||
|
||||
Uri uri;
|
||||
if (path[0] != '/' &&
|
||||
Uri.TryCreate(path, UriKind.Absolute, out var absoluteUri))
|
||||
{
|
||||
if (absoluteUri.Scheme != Uri.UriSchemeHttps)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Absolute BinaryLane API URLs must use the HTTPS scheme.",
|
||||
nameof(path));
|
||||
}
|
||||
|
||||
if (!HasSameOrigin(baseUri, absoluteUri))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Absolute BinaryLane API URLs must use the configured API origin.",
|
||||
nameof(path));
|
||||
}
|
||||
|
||||
uri = absoluteUri;
|
||||
}
|
||||
else
|
||||
{
|
||||
uri = new Uri(baseUri, path.TrimStart('/'));
|
||||
if (!HasSameOrigin(baseUri, uri))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Relative BinaryLane API paths must resolve on the configured API origin.",
|
||||
nameof(path));
|
||||
}
|
||||
}
|
||||
|
||||
if (query is null || query.Count == 0)
|
||||
{
|
||||
return uri;
|
||||
}
|
||||
|
||||
var builder = new UriBuilder(uri);
|
||||
var queryBuilder = new StringBuilder();
|
||||
if (!string.IsNullOrWhiteSpace(builder.Query))
|
||||
{
|
||||
queryBuilder.Append(builder.Query.TrimStart('?'));
|
||||
}
|
||||
|
||||
foreach (var pair in query)
|
||||
{
|
||||
if (pair.Value is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (queryBuilder.Length > 0)
|
||||
{
|
||||
queryBuilder.Append('&');
|
||||
}
|
||||
|
||||
queryBuilder.Append(Uri.EscapeDataString(pair.Key));
|
||||
queryBuilder.Append('=');
|
||||
queryBuilder.Append(Uri.EscapeDataString(FormatQueryValue(pair.Value)));
|
||||
}
|
||||
|
||||
builder.Query = queryBuilder.ToString();
|
||||
return builder.Uri;
|
||||
}
|
||||
|
||||
private static bool HasSameOrigin(Uri first, Uri second) =>
|
||||
string.Equals(first.Scheme, second.Scheme, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(first.Host, second.Host, StringComparison.OrdinalIgnoreCase) &&
|
||||
first.Port == second.Port;
|
||||
|
||||
private static string FormatQueryValue(object value)
|
||||
{
|
||||
if (value is bool boolean)
|
||||
{
|
||||
return boolean ? "true" : "false";
|
||||
}
|
||||
|
||||
if (value is DateTimeOffset dateTimeOffset)
|
||||
{
|
||||
return dateTimeOffset.ToString("O", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (value is DateTime dateTime)
|
||||
{
|
||||
return dateTime.ToString("O", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (value is IFormattable formattable)
|
||||
{
|
||||
return formattable.ToString(null, CultureInfo.InvariantCulture) ?? string.Empty;
|
||||
}
|
||||
|
||||
return value.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
private TResponse Deserialize<TResponse>(string body)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return default!;
|
||||
}
|
||||
|
||||
if (typeof(TResponse) == typeof(JsonElement))
|
||||
{
|
||||
using var document = JsonDocument.Parse(body);
|
||||
return (TResponse)(object)document.RootElement.Clone();
|
||||
}
|
||||
|
||||
var value = JsonSerializer.Deserialize<TResponse>(body, _json.SerializerOptions);
|
||||
if (value is null && typeof(TResponse).IsValueType)
|
||||
{
|
||||
throw new JsonException("BinaryLane returned a JSON null for a non-nullable response value.");
|
||||
}
|
||||
|
||||
return value!;
|
||||
}
|
||||
|
||||
private static ReadOnlyDictionary<string, IReadOnlyList<string>> CollectHeaders(HttpResponseMessage response)
|
||||
{
|
||||
var result = new Dictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var header in response.Headers)
|
||||
{
|
||||
result[header.Key] = new List<string>(header.Value).AsReadOnly();
|
||||
}
|
||||
|
||||
if (response.Content is not null)
|
||||
{
|
||||
foreach (var header in response.Content.Headers)
|
||||
{
|
||||
result[header.Key] = new List<string>(header.Value).AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
return new ReadOnlyDictionary<string, IReadOnlyList<string>>(result);
|
||||
}
|
||||
|
||||
private static BinaryLaneApiException CreateException(
|
||||
HttpStatusCode statusCode,
|
||||
Uri requestUri,
|
||||
string body,
|
||||
IReadOnlyDictionary<string, IReadOnlyList<string>> headers)
|
||||
{
|
||||
var problem = ParseProblem(body);
|
||||
var message = $"BinaryLane API request failed with HTTP {(int)statusCode} ({statusCode}).";
|
||||
var diagnosticBody = SanitizeAndTruncate(body);
|
||||
|
||||
if (statusCode == HttpStatusCode.BadRequest || statusCode == (HttpStatusCode)422)
|
||||
{
|
||||
return new BinaryLaneValidationException(message, statusCode, requestUri, problem, diagnosticBody, headers);
|
||||
}
|
||||
|
||||
return statusCode switch
|
||||
{
|
||||
HttpStatusCode.Unauthorized => new BinaryLaneUnauthorizedException(message, requestUri, problem, diagnosticBody, headers),
|
||||
HttpStatusCode.Forbidden => new BinaryLaneForbiddenException(message, requestUri, problem, diagnosticBody, headers),
|
||||
HttpStatusCode.NotFound => new BinaryLaneNotFoundException(message, requestUri, problem, diagnosticBody, headers),
|
||||
_ => new BinaryLaneApiException(message, statusCode, requestUri, problem, diagnosticBody, headers),
|
||||
};
|
||||
}
|
||||
|
||||
private static BinaryLaneApiProblem? ParseProblem(string body)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(body);
|
||||
var root = document.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BinaryLaneApiProblem
|
||||
{
|
||||
Type = ReadString(root, "type"),
|
||||
Title = ReadString(root, "title"),
|
||||
Detail = ReadString(root, "detail"),
|
||||
Status = ReadInt32(root, "status"),
|
||||
};
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ReadString(JsonElement element, string name) =>
|
||||
element.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.String
|
||||
? value.GetString()
|
||||
: null;
|
||||
|
||||
private static int? ReadInt32(JsonElement element, string name) =>
|
||||
element.TryGetProperty(name, out var value) && value.TryGetInt32(out var number)
|
||||
? number
|
||||
: null;
|
||||
|
||||
private static string? SanitizeAndTruncate(string body)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Do not expose an unbounded provider response in exception telemetry. Consumers should still
|
||||
// treat this diagnostic field as potentially sensitive and avoid logging it indiscriminately.
|
||||
if (body.Length <= MaximumDiagnosticBodyLength)
|
||||
{
|
||||
return body;
|
||||
}
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
return string.Concat(body.AsSpan(0, MaximumDiagnosticBodyLength), "…".AsSpan());
|
||||
#else
|
||||
return body.Substring(0, MaximumDiagnosticBodyLength) + "…";
|
||||
#endif
|
||||
}
|
||||
|
||||
private static async Task<string> ReadBodyAsync(HttpContent content, CancellationToken cancellationToken)
|
||||
{
|
||||
if (content.Headers.ContentLength is long contentLength && contentLength > MaximumSuccessBodyLength)
|
||||
{
|
||||
throw new HttpRequestException(
|
||||
$"BinaryLane response content exceeded the {MaximumSuccessBodyLength} byte limit.");
|
||||
}
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
await content.LoadIntoBufferAsync(MaximumSuccessBodyLength).ConfigureAwait(false);
|
||||
return await content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
await content.LoadIntoBufferAsync(MaximumSuccessBodyLength).ConfigureAwait(false);
|
||||
return await content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
private static async Task<string> ReadDiagnosticBodyAsync(HttpContent content, CancellationToken cancellationToken)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
using var stream = await content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
using var stream = await content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
#endif
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, true, 4096, leaveOpen: false);
|
||||
var buffer = new char[MaximumDiagnosticBodyLength + 1];
|
||||
var count = 0;
|
||||
|
||||
while (count < buffer.Length)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
var read = await reader
|
||||
.ReadAsync(buffer.AsMemory(count, buffer.Length - count), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#else
|
||||
var read = await reader
|
||||
.ReadAsync(buffer, count, buffer.Length - count)
|
||||
.ConfigureAwait(false);
|
||||
#endif
|
||||
if (read == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
count += read;
|
||||
}
|
||||
|
||||
return new string(buffer, 0, count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using BinaryLane.Api.V2.Models;
|
||||
|
||||
namespace BinaryLane.Api.V2.Http;
|
||||
|
||||
/// <summary>JSON settings used by the BinaryLane API client.</summary>
|
||||
public sealed class BinaryLaneJsonSerializerOptions
|
||||
{
|
||||
/// <summary>Creates the default BinaryLane JSON settings.</summary>
|
||||
public BinaryLaneJsonSerializerOptions()
|
||||
{
|
||||
SerializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
SerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
SerializerOptions.Converters.Add(new ServerActionJsonConverter());
|
||||
}
|
||||
|
||||
/// <summary>The serializer settings used for request and response bodies.</summary>
|
||||
public JsonSerializerOptions SerializerOptions { get; }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace BinaryLane.Api.V2.Http;
|
||||
|
||||
/// <summary>Represents one request to the BinaryLane v2 API.</summary>
|
||||
public sealed class BinaryLaneRequest
|
||||
{
|
||||
/// <summary>Initializes a request.</summary>
|
||||
public BinaryLaneRequest(HttpMethod method, string path)
|
||||
{
|
||||
Method = method ?? throw new ArgumentNullException(nameof(method));
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
throw new ArgumentException("An API path is required.", nameof(path));
|
||||
}
|
||||
|
||||
Path = path;
|
||||
}
|
||||
|
||||
/// <summary>HTTP method.</summary>
|
||||
public HttpMethod Method { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Relative v2 path (for example <c>v2/servers</c>) or an absolute HTTPS URL on the
|
||||
/// configured API origin. Relative paths are resolved against the configured
|
||||
/// <see cref="System.Net.Http.HttpClient.BaseAddress"/>. Other origins are rejected so a
|
||||
/// provider link cannot receive the configured bearer token.
|
||||
/// </summary>
|
||||
public string Path { get; }
|
||||
|
||||
/// <summary>Optional JSON request body.</summary>
|
||||
public object? Body { get; set; }
|
||||
|
||||
/// <summary>Optional query-string values. Null values are omitted.</summary>
|
||||
public IReadOnlyDictionary<string, object?>? Query { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional request headers. Authorization is supplied by the configured token provider and
|
||||
/// cannot be overridden here.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string>? Headers { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
||||
namespace BinaryLane.Api.V2.Http;
|
||||
|
||||
/// <summary>Response metadata and a deserialized BinaryLane API body.</summary>
|
||||
/// <typeparam name="T">The deserialized response type.</typeparam>
|
||||
public sealed class BinaryLaneResponse<T>
|
||||
{
|
||||
internal BinaryLaneResponse(
|
||||
T body,
|
||||
HttpStatusCode statusCode,
|
||||
Uri requestUri,
|
||||
IReadOnlyDictionary<string, IReadOnlyList<string>> headers)
|
||||
{
|
||||
Body = body;
|
||||
StatusCode = statusCode;
|
||||
RequestUri = requestUri;
|
||||
Headers = headers;
|
||||
}
|
||||
|
||||
/// <summary>Deserialized response body.</summary>
|
||||
public T Body { get; }
|
||||
|
||||
/// <summary>HTTP status code returned by BinaryLane.</summary>
|
||||
public HttpStatusCode StatusCode { get; }
|
||||
|
||||
/// <summary>Final request URI.</summary>
|
||||
public Uri RequestUri { get; }
|
||||
|
||||
/// <summary>Response and content headers.</summary>
|
||||
public IReadOnlyDictionary<string, IReadOnlyList<string>> Headers { get; }
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BinaryLane.Api.V2.Http;
|
||||
|
||||
/// <summary>
|
||||
/// Low-level request executor used by resource APIs. It is public so consuming applications can
|
||||
/// compose support for a newly introduced BinaryLane endpoint before a typed resource is released.
|
||||
/// </summary>
|
||||
public interface IBinaryLaneApiExecutor
|
||||
{
|
||||
/// <summary>Sends a request and returns response metadata and the deserialized body.</summary>
|
||||
Task<BinaryLaneResponse<TResponse>> SendAsync<TResponse>(
|
||||
BinaryLaneRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Sends a request which is expected to have no response body.</summary>
|
||||
Task<BinaryLaneResponse<object?>> SendAsync(
|
||||
BinaryLaneRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Sends a GET request.</summary>
|
||||
Task<TResponse> GetAsync<TResponse>(
|
||||
string path,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Sends a POST request.</summary>
|
||||
Task<TResponse> PostAsync<TResponse>(
|
||||
string path,
|
||||
object? body = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Sends a PUT request.</summary>
|
||||
Task<TResponse> PutAsync<TResponse>(
|
||||
string path,
|
||||
object? body = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Sends a PATCH request.</summary>
|
||||
Task<TResponse> PatchAsync<TResponse>(
|
||||
string path,
|
||||
object? body = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Sends a DELETE request.</summary>
|
||||
Task DeleteAsync(
|
||||
string path,
|
||||
object? body = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
Reference in New Issue
Block a user