Enhanced ReCClientHelpers.cs with a new utility method: - Added `BuildQueryFromObject<T>` to serialize objects into query strings. - Skips `null` properties and escapes names/values for safety. - Added `System.Collections` and `System.Reflection` namespaces to support reflection and collection operations. This improves HTTP request handling by enabling dynamic query string generation from object payloads.
138 lines
5.9 KiB
C#
138 lines
5.9 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Provides shared helpers for composing requests.
|
|
/// </summary>
|
|
internal static class ReCClientHelpers
|
|
{
|
|
#if NETFRAMEWORK
|
|
/// <summary>
|
|
/// Builds a query string from the provided key/value pairs, skipping null values.
|
|
/// </summary>
|
|
/// <param name="parameters">The key/value pairs to include in the query string.</param>
|
|
/// <returns>A query string beginning with '?', or an empty string if no values are provided.</returns>
|
|
public static string BuildQuery(params (string Key, object Value)[] parameters)
|
|
#else
|
|
/// <summary>
|
|
/// Builds a query string from the provided key/value pairs, skipping null values.
|
|
/// </summary>
|
|
/// <param name="parameters">The key/value pairs to include in the query string.</param>
|
|
/// <returns>A query string beginning with '?', or an empty string if no values are provided.</returns>
|
|
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}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a JSON content payload from the provided object.
|
|
/// </summary>
|
|
/// <typeparam name="T">The type of the payload.</typeparam>
|
|
/// <param name="payload">The payload to serialize.</param>
|
|
/// <returns>A <see cref="JsonContent"/> instance ready for HTTP requests.</returns>
|
|
public static JsonContent ToJsonContent<T>(T payload) => JsonContent.Create(payload);
|
|
|
|
/// <summary>
|
|
/// Builds a query string from the public readable properties of <paramref name="payload"/>,
|
|
/// skipping properties whose values are <see langword="null"/>.
|
|
/// </summary>
|
|
/// <typeparam name="T">The payload type.</typeparam>
|
|
/// <param name="payload">The payload to serialize into a query string.</param>
|
|
/// <returns>A query string beginning with '?', or an empty string if no values are provided.</returns>
|
|
public static string BuildQueryFromObject<T>(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}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Logs the outcome of an HTTP response. Throws a <see cref="ReCApiException"/> when the
|
|
/// response indicates a non-success status code; otherwise (optionally) writes an informational
|
|
/// log entry containing the request and response details.
|
|
/// </summary>
|
|
/// <param name="response">The HTTP response to inspect.</param>
|
|
/// <param name="logger">An optional logger used to record the outcome. May be <see langword="null"/>.</param>
|
|
/// <param name="logSuccess">When <see langword="false"/>, successful responses are not logged.</param>
|
|
/// <param name="cancel">A token to cancel the operation.</param>
|
|
#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);
|
|
}
|
|
}
|
|
}
|