using System; using System.Collections; using System.Globalization; using System.Linq; using System.Net.Http; using System.Net.Http.Json; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace ReC.Client { /// /// Provides shared helpers for composing requests. /// internal static class ReCClientHelpers { #if NETFRAMEWORK /// /// Builds a query string from the provided key/value pairs, skipping null values. /// /// The key/value pairs to include in the query string. /// A query string beginning with '?', or an empty string if no values are provided. public static string BuildQuery(params (string Key, object Value)[] parameters) #else /// /// Builds a query string from the provided key/value pairs, skipping null values. /// /// The key/value pairs to include in the query string. /// A query string beginning with '?', or an empty string if no values are provided. public static string BuildQuery(params (string Key, object? Value)[] parameters) #endif { var parts = parameters .Where(p => p.Value != null) .Select(p => $"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(Convert.ToString(p.Value, CultureInfo.InvariantCulture) ?? string.Empty)}"); var query = string.Join("&", parts); return string.IsNullOrWhiteSpace(query) ? string.Empty : $"?{query}"; } /// /// Creates a JSON content payload from the provided object. /// /// The type of the payload. /// The payload to serialize. /// A instance ready for HTTP requests. public static JsonContent ToJsonContent(T payload) => JsonContent.Create(payload); /// /// Builds a query string from the public readable properties of , /// skipping properties whose values are . /// /// The payload type. /// The payload to serialize into a query string. /// A query string beginning with '?', or an empty string if no values are provided. public static string BuildQueryFromObject(T payload) { if (payload == null) return string.Empty; var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.CanRead && p.GetIndexParameters().Length == 0); var parts = props .Select(p => new { p.Name, Value = p.GetValue(payload) }) .Where(p => p.Value != null) .Select(p => $"{Uri.EscapeDataString(p.Name)}={Uri.EscapeDataString(Convert.ToString(p.Value, CultureInfo.InvariantCulture) ?? string.Empty)}"); var query = string.Join("&", parts); return string.IsNullOrWhiteSpace(query) ? string.Empty : $"?{query}"; } /// /// Logs the outcome of an HTTP response. Throws a when the /// response indicates a non-success status code; otherwise (optionally) writes an informational /// log entry containing the request and response details. /// /// The HTTP response to inspect. /// An optional logger used to record the outcome. May be . /// When , successful responses are not logged. /// A token to cancel the operation. #if NETFRAMEWORK public static async Task HandleResponseAsync(HttpResponseMessage response, ILogger logger = null, bool logSuccess = true, CancellationToken cancel = default) #else public static async Task HandleResponseAsync(HttpResponseMessage response, ILogger? logger = null, bool logSuccess = true, CancellationToken cancel = default) #endif { var request = response.RequestMessage; var method = request?.Method?.Method; var uri = request?.RequestUri; var statusCode = (int)response.StatusCode; if (response.IsSuccessStatusCode) { if (logSuccess) { logger?.LogInformation( "ReC API request succeeded. {Method} {Uri} -> {StatusCode} ({ReasonPhrase})", method, uri, statusCode, response.ReasonPhrase); } return; } #if NETFRAMEWORK string body = null; #else string? body = null; #endif if (response.Content != null) { try { #if NETFRAMEWORK body = await response.Content.ReadAsStringAsync().ConfigureAwait(false); #else body = await response.Content.ReadAsStringAsync(cancel).ConfigureAwait(false); #endif } catch { // Swallow body read failures; status info is still propagated. } } var message = $"ReC API request failed with status {statusCode} ({response.ReasonPhrase}). " + $"{method} {uri}" + (string.IsNullOrWhiteSpace(body) ? string.Empty : $": {body}"); throw new ReCApiException(message, response.StatusCode, response.ReasonPhrase, body, method, uri); } } }