14 Commits

Author SHA1 Message Date
6d8e51ad70 Add ReCClientOptions for configurable client behavior
Introduced a new `ReCClientOptions` property to `BaseCrudApi`
and its derived classes to enhance flexibility and control
over client behavior. Updated constructors to accept an
optional `ReCClientOptions` parameter, with default options
applied when omitted.

Modified `CreateAsync`, `UpdateAsync`, and other methods in
`BaseCrudApi` to utilize the `Options.LogSuccessfulRequests`
property for more granular logging control. Updated derived
API classes (`CommonApi`, `EndpointAuthApi`, `EndpointParamsApi`,
`EndpointsApi`, `ProfileApi`, `RecActionApi`, and `ResultApi`)
to pass the `options` parameter to the base constructor.

Ensured compatibility with both `NETFRAMEWORK` and other
frameworks by using nullable annotations where applicable.
These changes improve the extensibility and maintainability
of the API client.
2026-05-19 19:20:54 +02:00
01ac7ece1e Add optional logging control to HandleResponseAsync
The HandleResponseAsync method was updated to include a new
optional parameter, `logSuccess`, which allows control over
whether successful HTTP responses are logged. The default
value is `true`. This change applies to both `NETFRAMEWORK`
and non-`NETFRAMEWORK` builds. The method's XML documentation
was updated to reflect this new behavior.
2026-05-19 19:20:37 +02:00
e0c2aab2b1 Add support for configurable options in ReCClient
Updated `ReCClient` to support dependency injection for
`IOptions<ReCClientOptions>` and `ILogger`. Modified the
constructor to include an optional `IOptions` parameter,
allowing the use of configurable client options with
default values when omitted. Updated API component
initialization to pass `ReCClientOptions` for enhanced
configuration.

Added `Microsoft.Extensions.Logging` and
`Microsoft.Extensions.Options` to `using` directives.
Ensured compatibility with both `NETFRAMEWORK` and other
target frameworks by updating constructor signatures
accordingly.
2026-05-19 19:20:28 +02:00
a43d1ebc20 Add optional ReCClientOptions configuration support
Added an optional `configureOptions` parameter to `AddRecClient`
methods, enabling configuration of `ReCClientOptions`. Introduced
conditional compilation to handle nullability differences between
.NET Framework and other frameworks.

Implemented a private helper method `AddRecClientOptions` to ensure
default options are registered even when no configuration action is
provided. Updated `AddRecClient` overloads to use this helper.

Included `System.Net.Http` in `#if NETFRAMEWORK` directives to
maintain compatibility with .NET Framework.
2026-05-19 19:20:17 +02:00
f96ad1ac7e Add ReCClientOptions for configurable logging behavior
Introduce the `ReCClientOptions` class in the new `ReC.Client`
namespace. This class includes the `LogSuccessfulRequests`
property, which allows users to enable or disable logging for
successful API requests via the injected `ILogger`. Failed
requests are unaffected and will always throw `ReCApiException`.
The property defaults to `true`. XML documentation is included
to describe the class and its behavior.
2026-05-19 19:19:32 +02:00
20766091a9 Add logging to HandleResponseAsync in ReCClientHelpers
Refactored the `EnsureSuccessAsync` method to `HandleResponseAsync`
and added optional `ILogger` support for logging HTTP request
and response details.

- Added `using Microsoft.Extensions.Logging;` for logging.
- Log success responses with HTTP method, URI, status code,
  and reason phrase.
- Updated exception message construction for clarity.
- Added conditional compilation for nullable `ILogger?`
  in non-NET Framework targets.
- Improved code maintainability by consolidating logic.
2026-05-19 19:09:51 +02:00
91c166dc4d Add ILogger support for enhanced API call logging
Introduced optional ILogger support across BaseCrudApi and its
derived classes to enable logging of API call outcomes. Updated
constructors to accept an optional ILogger parameter, with
conditional compilation for .NET Framework compatibility.

Replaced EnsureSuccessAsync with HandleResponseAsync in CRUD
methods to integrate logging. Updated derived API classes
(CommonApi, EndpointAuthApi, EndpointParamsApi, EndpointsApi,
ProfileApi, RecActionApi, ResultApi) to pass ILogger to the base
class.

Added Microsoft.Extensions.Logging imports and ensured backward
compatibility by making ILogger optional and handling nullable
reference types in non-.NET Framework environments.
2026-05-19 19:09:08 +02:00
7ed348832c Add logging support to ReCClient and related APIs
Updated the `ReCClient` constructor to include an optional `ILogger` parameter for logging API call outcomes. Added support for both .NET Framework and other frameworks by using non-generic and generic `ILogger` types, respectively. Updated API-related objects (`RecActionApi`, `ResultApi`, etc.) to accept and utilize the `ILogger` instance for enhanced logging functionality.
2026-05-19 19:08:30 +02:00
190d41489e Refactor InvokeAsync to return Task and improve docs
The return type of the `InvokeAsync` method has been changed from `Task<bool>` to `Task` for both overloads, removing the boolean return value for success indication.

The `<returns>` XML documentation tag has been removed, and a new `<exception>` tag has been added to document the potential `ReCApiException` thrown when the API responds with a non-success status code.

The implementation now uses `ReCClientHelpers.EnsureSuccessAsync` to handle API responses, replacing the previous `resp.IsSuccessStatusCode` check.

These changes improve clarity and align the method's behavior with standard practices for handling asynchronous operations and exceptions.
2026-05-19 18:57:33 +02:00
dfcf1fb536 Refactor BaseCrudApi methods to improve error handling
Updated `CreateAsync`, `UpdateAsync`, and `DeleteAsync` methods to return `Task` instead of `Task<bool>`. Removed `<returns>` documentation and added `<exception>` tags to indicate that a `ReCApiException` is thrown for non-successful API responses. Replaced `resp.IsSuccessStatusCode` checks with `ReCClientHelpers.EnsureSuccessAsync` to enforce exception-based error handling. These changes align with modern asynchronous error-handling practices.
2026-05-19 18:57:21 +02:00
136c2fcb30 Add EnsureSuccessAsync method for HTTP error handling
Introduced the `EnsureSuccessAsync` method in `ReCClientHelpers.cs` to handle HTTP response validation asynchronously. This method throws a `ReCApiException` for non-success status codes, including detailed error information such as status code, reason phrase, HTTP method, URI, and response body (if available).

Updated `using` directives to support asynchronous operations and cancellation tokens. Removed redundant `#if NETFRAMEWORK` directive around `using System.Net.Http;` and adjusted `using System.Net.Http.Json;` placement for consistency.

Added exception handling for response body read failures to ensure status information is still propagated. Enhanced error reporting for failed HTTP requests.
2026-05-19 18:57:05 +02:00
71defc0e4c Add ReCApiException class for API error handling
A new `ReCApiException` class was introduced in the `ReC.Client` namespace to represent errors returned by the ReC API.

The class includes properties for detailed error information:
- `StatusCode`, `ReasonPhrase`, `ResponseBody`, `Method`, and `RequestUri`.

A constructor was added to initialize these properties. Conditional compilation directives ensure compatibility between .NET Framework and other .NET targets. The class is marked as `[Serializable]` for non-.NET Framework targets.
2026-05-19 18:56:43 +02:00
992395dec3 Ensure proper disposal of resources in InvokeAsync
Updated the `InvokeAsync` method in the `ReC.Client.Api` namespace to use `using` statements for the `content` and `resp` objects. This change ensures proper disposal of these resources, improving memory management and preventing potential leaks. The functional behavior of the method remains unchanged.
2026-05-19 18:44:55 +02:00
82ec333f23 Refactor API methods to return bool for success status
Updated `CreateAsync<T>`, `UpdateAsync<T>`, and `DeleteAsync<T>`
methods to return a `bool` indicating success instead of
`HttpResponseMessage`. Added `using` statements to ensure proper
disposal of HTTP content and response objects. Simplified the
interface for better usability by leveraging `IsSuccessStatusCode`
to determine operation success.
2026-05-19 18:44:44 +02:00
13 changed files with 330 additions and 41 deletions

View File

@@ -2,6 +2,7 @@ using System;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace ReC.Client.Api namespace ReC.Client.Api
{ {
@@ -20,15 +21,37 @@ namespace ReC.Client.Api
/// </summary> /// </summary>
protected readonly string ResourcePath; protected readonly string ResourcePath;
/// <summary>
/// An optional logger used to record API call outcomes. May be <see langword="null"/>.
/// </summary>
#if NETFRAMEWORK
protected readonly ILogger Logger;
#else
protected readonly ILogger? Logger;
#endif
/// <summary>
/// The options controlling client behavior. Never <see langword="null"/>.
/// </summary>
protected readonly ReCClientOptions Options;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BaseCrudApi"/> class. /// Initializes a new instance of the <see cref="BaseCrudApi"/> class.
/// </summary> /// </summary>
/// <param name="http">The HTTP client used for requests.</param> /// <param name="http">The HTTP client used for requests.</param>
/// <param name="resourcePath">The base resource path for the API endpoint.</param> /// <param name="resourcePath">The base resource path for the API endpoint.</param>
protected BaseCrudApi(HttpClient http, string resourcePath) /// <param name="logger">An optional logger used to record API call outcomes.</param>
/// <param name="options">An optional set of client options. Defaults are used when omitted.</param>
#if NETFRAMEWORK
protected BaseCrudApi(HttpClient http, string resourcePath, ILogger logger = null, ReCClientOptions options = null)
#else
protected BaseCrudApi(HttpClient http, string resourcePath, ILogger? logger = null, ReCClientOptions? options = null)
#endif
{ {
Http = http ?? throw new ArgumentNullException(nameof(http)); Http = http ?? throw new ArgumentNullException(nameof(http));
ResourcePath = resourcePath ?? throw new ArgumentNullException(nameof(resourcePath)); ResourcePath = resourcePath ?? throw new ArgumentNullException(nameof(resourcePath));
Logger = logger;
Options = options ?? new ReCClientOptions();
} }
/// <summary> /// <summary>
@@ -37,9 +60,15 @@ namespace ReC.Client.Api
/// <typeparam name="T">The payload type.</typeparam> /// <typeparam name="T">The payload type.</typeparam>
/// <param name="payload">The payload to send.</param> /// <param name="payload">The payload to send.</param>
/// <param name="cancel">A token to cancel the operation.</param> /// <param name="cancel">A token to cancel the operation.</param>
/// <returns>The HTTP response message.</returns> /// <exception cref="ReCApiException">Thrown when the API responds with a non-success status code.</exception>
public Task<HttpResponseMessage> CreateAsync<T>(T payload, CancellationToken cancel = default) public async Task CreateAsync<T>(T payload, CancellationToken cancel = default)
=> Http.PostAsync(ResourcePath, ReCClientHelpers.ToJsonContent(payload), cancel); {
using (var content = ReCClientHelpers.ToJsonContent(payload))
using (var resp = await Http.PostAsync(ResourcePath, content, cancel))
{
await ReCClientHelpers.HandleResponseAsync(resp, Logger, Options.LogSuccessfulRequests, cancel).ConfigureAwait(false);
}
}
/// <summary> /// <summary>
/// Updates a resource by identifier. /// Updates a resource by identifier.
@@ -48,9 +77,15 @@ namespace ReC.Client.Api
/// <param name="id">The resource identifier.</param> /// <param name="id">The resource identifier.</param>
/// <param name="payload">The payload to send.</param> /// <param name="payload">The payload to send.</param>
/// <param name="cancel">A token to cancel the operation.</param> /// <param name="cancel">A token to cancel the operation.</param>
/// <returns>The HTTP response message.</returns> /// <exception cref="ReCApiException">Thrown when the API responds with a non-success status code.</exception>
public Task<HttpResponseMessage> UpdateAsync<T>(long id, T payload, CancellationToken cancel = default) public async Task UpdateAsync<T>(long id, T payload, CancellationToken cancel = default)
=> Http.PutAsync($"{ResourcePath}/{id}", ReCClientHelpers.ToJsonContent(payload), cancel); {
using (var content = ReCClientHelpers.ToJsonContent(payload))
using (var resp = await Http.PutAsync($"{ResourcePath}/{id}", content, cancel))
{
await ReCClientHelpers.HandleResponseAsync(resp, Logger, Options.LogSuccessfulRequests, cancel).ConfigureAwait(false);
}
}
/// <summary> /// <summary>
/// Deletes resources with identifiers supplied in the payload. /// Deletes resources with identifiers supplied in the payload.
@@ -58,14 +93,17 @@ namespace ReC.Client.Api
/// <typeparam name="T">The payload type containing identifiers.</typeparam> /// <typeparam name="T">The payload type containing identifiers.</typeparam>
/// <param name="payload">The payload to send.</param> /// <param name="payload">The payload to send.</param>
/// <param name="cancel">A token to cancel the operation.</param> /// <param name="cancel">A token to cancel the operation.</param>
/// <returns>The HTTP response message.</returns> /// <exception cref="ReCApiException">Thrown when the API responds with a non-success status code.</exception>
public Task<HttpResponseMessage> DeleteAsync<T>(T payload, CancellationToken cancel = default) public async Task DeleteAsync<T>(T payload, CancellationToken cancel = default)
{ {
var request = new HttpRequestMessage(HttpMethod.Delete, ResourcePath) using (var request = new HttpRequestMessage(HttpMethod.Delete, ResourcePath)
{ {
Content = ReCClientHelpers.ToJsonContent(payload) Content = ReCClientHelpers.ToJsonContent(payload)
}; })
return Http.SendAsync(request, cancel); using (var resp = await Http.SendAsync(request, cancel))
{
await ReCClientHelpers.HandleResponseAsync(resp, Logger, Options.LogSuccessfulRequests, cancel).ConfigureAwait(false);
}
} }
} }
} }

View File

@@ -1,6 +1,7 @@
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace ReC.Client.Api namespace ReC.Client.Api
{ {
@@ -13,7 +14,13 @@ namespace ReC.Client.Api
/// Initializes a new instance of the <see cref="CommonApi"/> class. /// Initializes a new instance of the <see cref="CommonApi"/> class.
/// </summary> /// </summary>
/// <param name="http">The HTTP client used for requests.</param> /// <param name="http">The HTTP client used for requests.</param>
public CommonApi(HttpClient http) : base(http, "api/Common") /// <param name="logger">An optional logger used to record API call outcomes.</param>
/// <param name="options">An optional set of client options. Defaults are used when omitted.</param>
#if NETFRAMEWORK
public CommonApi(HttpClient http, ILogger logger = null, ReCClientOptions options = null) : base(http, "api/Common", logger, options)
#else
public CommonApi(HttpClient http, ILogger? logger = null, ReCClientOptions? options = null) : base(http, "api/Common", logger, options)
#endif
{ {
} }
} }

View File

@@ -1,6 +1,7 @@
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace ReC.Client.Api namespace ReC.Client.Api
{ {
@@ -13,7 +14,13 @@ namespace ReC.Client.Api
/// Initializes a new instance of the <see cref="EndpointAuthApi"/> class. /// Initializes a new instance of the <see cref="EndpointAuthApi"/> class.
/// </summary> /// </summary>
/// <param name="http">The HTTP client used for requests.</param> /// <param name="http">The HTTP client used for requests.</param>
public EndpointAuthApi(HttpClient http) : base(http, "api/EndpointAuth") /// <param name="logger">An optional logger used to record API call outcomes.</param>
/// <param name="options">An optional set of client options. Defaults are used when omitted.</param>
#if NETFRAMEWORK
public EndpointAuthApi(HttpClient http, ILogger logger = null, ReCClientOptions options = null) : base(http, "api/EndpointAuth", logger, options)
#else
public EndpointAuthApi(HttpClient http, ILogger? logger = null, ReCClientOptions? options = null) : base(http, "api/EndpointAuth", logger, options)
#endif
{ {
} }
} }

View File

@@ -1,6 +1,7 @@
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace ReC.Client.Api namespace ReC.Client.Api
{ {
@@ -13,7 +14,13 @@ namespace ReC.Client.Api
/// Initializes a new instance of the <see cref="EndpointParamsApi"/> class. /// Initializes a new instance of the <see cref="EndpointParamsApi"/> class.
/// </summary> /// </summary>
/// <param name="http">The HTTP client used for requests.</param> /// <param name="http">The HTTP client used for requests.</param>
public EndpointParamsApi(HttpClient http) : base(http, "api/EndpointParams") /// <param name="logger">An optional logger used to record API call outcomes.</param>
/// <param name="options">An optional set of client options. Defaults are used when omitted.</param>
#if NETFRAMEWORK
public EndpointParamsApi(HttpClient http, ILogger logger = null, ReCClientOptions options = null) : base(http, "api/EndpointParams", logger, options)
#else
public EndpointParamsApi(HttpClient http, ILogger? logger = null, ReCClientOptions? options = null) : base(http, "api/EndpointParams", logger, options)
#endif
{ {
} }
} }

View File

@@ -1,6 +1,7 @@
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace ReC.Client.Api namespace ReC.Client.Api
{ {
@@ -13,7 +14,13 @@ namespace ReC.Client.Api
/// Initializes a new instance of the <see cref="EndpointsApi"/> class. /// Initializes a new instance of the <see cref="EndpointsApi"/> class.
/// </summary> /// </summary>
/// <param name="http">The HTTP client used for requests.</param> /// <param name="http">The HTTP client used for requests.</param>
public EndpointsApi(HttpClient http) : base(http, "api/Endpoints") /// <param name="logger">An optional logger used to record API call outcomes.</param>
/// <param name="options">An optional set of client options. Defaults are used when omitted.</param>
#if NETFRAMEWORK
public EndpointsApi(HttpClient http, ILogger logger = null, ReCClientOptions options = null) : base(http, "api/Endpoints", logger, options)
#else
public EndpointsApi(HttpClient http, ILogger? logger = null, ReCClientOptions? options = null) : base(http, "api/Endpoints", logger, options)
#endif
{ {
} }
} }

View File

@@ -1,6 +1,7 @@
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace ReC.Client.Api namespace ReC.Client.Api
{ {
@@ -13,7 +14,13 @@ namespace ReC.Client.Api
/// Initializes a new instance of the <see cref="ProfileApi"/> class. /// Initializes a new instance of the <see cref="ProfileApi"/> class.
/// </summary> /// </summary>
/// <param name="http">The HTTP client used for requests.</param> /// <param name="http">The HTTP client used for requests.</param>
public ProfileApi(HttpClient http) : base(http, "api/Profile") /// <param name="logger">An optional logger used to record API call outcomes.</param>
/// <param name="options">An optional set of client options. Defaults are used when omitted.</param>
#if NETFRAMEWORK
public ProfileApi(HttpClient http, ILogger logger = null, ReCClientOptions options = null) : base(http, "api/Profile", logger, options)
#else
public ProfileApi(HttpClient http, ILogger? logger = null, ReCClientOptions? options = null) : base(http, "api/Profile", logger, options)
#endif
{ {
} }

View File

@@ -1,6 +1,7 @@
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace ReC.Client.Api namespace ReC.Client.Api
{ {
@@ -13,7 +14,13 @@ namespace ReC.Client.Api
/// Initializes a new instance of the <see cref="RecActionApi"/> class. /// Initializes a new instance of the <see cref="RecActionApi"/> class.
/// </summary> /// </summary>
/// <param name="http">The HTTP client used for requests.</param> /// <param name="http">The HTTP client used for requests.</param>
public RecActionApi(HttpClient http) : base(http, "api/RecAction") /// <param name="logger">An optional logger used to record API call outcomes.</param>
/// <param name="options">An optional set of client options. Defaults are used when omitted.</param>
#if NETFRAMEWORK
public RecActionApi(HttpClient http, ILogger logger = null, ReCClientOptions options = null) : base(http, "api/RecAction", logger, options)
#else
public RecActionApi(HttpClient http, ILogger? logger = null, ReCClientOptions? options = null) : base(http, "api/RecAction", logger, options)
#endif
{ {
} }
@@ -23,12 +30,15 @@ namespace ReC.Client.Api
/// <param name="profileId">The profile identifier.</param> /// <param name="profileId">The profile identifier.</param>
/// <param name="references">Optional reference values to pass through to all result records.</param> /// <param name="references">Optional reference values to pass through to all result records.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param> /// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns><see langword="true"/> if the request succeeds; otherwise, <see langword="false"/>.</returns> /// <exception cref="ReCApiException">Thrown when the API responds with a non-success status code.</exception>
public async Task<bool> InvokeAsync(int profileId, InvokeReferences references, CancellationToken cancellationToken = default) public async Task InvokeAsync(int profileId, InvokeReferences references, CancellationToken cancellationToken = default)
{ {
var content = references != null ? ReCClientHelpers.ToJsonContent(references) : null; var content = references != null ? ReCClientHelpers.ToJsonContent(references) : null;
var resp = await Http.PostAsync($"{ResourcePath}/invoke/{profileId}", content, cancellationToken); using (content)
return resp.IsSuccessStatusCode; using (var resp = await Http.PostAsync($"{ResourcePath}/invoke/{profileId}", content, cancellationToken))
{
await ReCClientHelpers.HandleResponseAsync(resp, Logger, Options.LogSuccessfulRequests, cancellationToken).ConfigureAwait(false);
}
} }
/// <summary> /// <summary>
@@ -37,8 +47,8 @@ namespace ReC.Client.Api
/// <param name="profileId">The profile identifier.</param> /// <param name="profileId">The profile identifier.</param>
/// <param name="batchId">Batch identifier.</param> /// <param name="batchId">Batch identifier.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param> /// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns><see langword="true"/> if the request succeeds; otherwise, <see langword="false"/>.</returns> /// <exception cref="ReCApiException">Thrown when the API responds with a non-success status code.</exception>
public Task<bool> InvokeAsync(int profileId, string batchId, CancellationToken cancellationToken = default) public Task InvokeAsync(int profileId, string batchId, CancellationToken cancellationToken = default)
{ {
return InvokeAsync(profileId, new InvokeReferences() { BatchId = batchId }, cancellationToken); return InvokeAsync(profileId, new InvokeReferences() { BatchId = batchId }, cancellationToken);
} }

View File

@@ -1,6 +1,7 @@
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace ReC.Client.Api namespace ReC.Client.Api
{ {
@@ -13,7 +14,13 @@ namespace ReC.Client.Api
/// Initializes a new instance of the <see cref="ResultApi"/> class. /// Initializes a new instance of the <see cref="ResultApi"/> class.
/// </summary> /// </summary>
/// <param name="http">The HTTP client used for requests.</param> /// <param name="http">The HTTP client used for requests.</param>
public ResultApi(HttpClient http) : base(http, "api/Result") /// <param name="logger">An optional logger used to record API call outcomes.</param>
/// <param name="options">An optional set of client options. Defaults are used when omitted.</param>
#if NETFRAMEWORK
public ResultApi(HttpClient http, ILogger logger = null, ReCClientOptions options = null) : base(http, "api/Result", logger, options)
#else
public ResultApi(HttpClient http, ILogger? logger = null, ReCClientOptions? options = null) : base(http, "api/Result", logger, options)
#endif
{ {
} }

View File

@@ -1,6 +1,6 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
#if NETFRAMEWORK
using System; using System;
#if NETFRAMEWORK
using System.Net.Http; using System.Net.Http;
#endif #endif
@@ -16,9 +16,15 @@ namespace ReC.Client
/// </summary> /// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param> /// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <param name="apiUri">The base URI of the ReC API.</param> /// <param name="apiUri">The base URI of the ReC API.</param>
/// <param name="configureOptions">An optional action to configure <see cref="ReCClientOptions"/>. When omitted, defaults are used.</param>
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns> /// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
public static IHttpClientBuilder AddRecClient(this IServiceCollection services, string apiUri) #if NETFRAMEWORK
public static IHttpClientBuilder AddRecClient(this IServiceCollection services, string apiUri, Action<ReCClientOptions> configureOptions = null)
#else
public static IHttpClientBuilder AddRecClient(this IServiceCollection services, string apiUri, Action<ReCClientOptions>? configureOptions = null)
#endif
{ {
AddRecClientOptions(services, configureOptions);
services.AddScoped<ReCClient>(); services.AddScoped<ReCClient>();
return services.AddHttpClient(ReCClient.ClientName, client => return services.AddHttpClient(ReCClient.ClientName, client =>
{ {
@@ -31,11 +37,29 @@ namespace ReC.Client
/// </summary> /// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param> /// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <param name="configureClient">An action to configure the <see cref="HttpClient"/>.</param> /// <param name="configureClient">An action to configure the <see cref="HttpClient"/>.</param>
/// <param name="configureOptions">An optional action to configure <see cref="ReCClientOptions"/>. When omitted, defaults are used.</param>
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns> /// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
public static IHttpClientBuilder AddRecClient(this IServiceCollection services, Action<HttpClient> configureClient) #if NETFRAMEWORK
public static IHttpClientBuilder AddRecClient(this IServiceCollection services, Action<HttpClient> configureClient, Action<ReCClientOptions> configureOptions = null)
#else
public static IHttpClientBuilder AddRecClient(this IServiceCollection services, Action<HttpClient> configureClient, Action<ReCClientOptions>? configureOptions = null)
#endif
{ {
AddRecClientOptions(services, configureOptions);
services.AddScoped<ReCClient>(); services.AddScoped<ReCClient>();
return services.AddHttpClient(ReCClient.ClientName, configureClient); return services.AddHttpClient(ReCClient.ClientName, configureClient);
} }
#if NETFRAMEWORK
private static void AddRecClientOptions(IServiceCollection services, Action<ReCClientOptions> configureOptions)
#else
private static void AddRecClientOptions(IServiceCollection services, Action<ReCClientOptions>? configureOptions)
#endif
{
// Ensure default options are always registered even when the caller does not configure anything.
var builder = services.AddOptions<ReCClientOptions>();
if (configureOptions != null)
builder.Configure(configureOptions);
}
} }
} }

View File

@@ -0,0 +1,88 @@
using System;
using System.Net;
using System.Net.Http;
namespace ReC.Client
{
/// <summary>
/// Represents an error returned by the ReC API.
/// </summary>
#if !NETFRAMEWORK
[Serializable]
#endif
public class ReCApiException : Exception
{
/// <summary>
/// The HTTP status code returned by the API.
/// </summary>
public HttpStatusCode StatusCode { get; }
/// <summary>
/// The HTTP reason phrase returned by the API, if any.
/// </summary>
#if NETFRAMEWORK
public string ReasonPhrase { get; }
#else
public string? ReasonPhrase { get; }
#endif
/// <summary>
/// The raw response body returned by the API, if any.
/// </summary>
#if NETFRAMEWORK
public string ResponseBody { get; }
#else
public string? ResponseBody { get; }
#endif
/// <summary>
/// The HTTP method used for the failed request.
/// </summary>
#if NETFRAMEWORK
public string Method { get; }
#else
public string? Method { get; }
#endif
/// <summary>
/// The request URI that was called.
/// </summary>
#if NETFRAMEWORK
public Uri RequestUri { get; }
#else
public Uri? RequestUri { get; }
#endif
/// <summary>
/// Initializes a new instance of the <see cref="ReCApiException"/> class.
/// </summary>
/// <param name="message">A summary message describing the error.</param>
/// <param name="statusCode">The HTTP status code returned by the API.</param>
/// <param name="reasonPhrase">The HTTP reason phrase returned by the API.</param>
/// <param name="responseBody">The raw response body returned by the API.</param>
/// <param name="method">The HTTP method used for the request.</param>
/// <param name="requestUri">The request URI that was called.</param>
public ReCApiException(
string message,
HttpStatusCode statusCode,
#if NETFRAMEWORK
string reasonPhrase,
string responseBody,
string method,
Uri requestUri
#else
string? reasonPhrase,
string? responseBody,
string? method,
Uri? requestUri
#endif
) : base(message)
{
StatusCode = statusCode;
ReasonPhrase = reasonPhrase;
ResponseBody = responseBody;
Method = method;
RequestUri = requestUri;
}
}
}

View File

@@ -1,4 +1,6 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System; using System;
using System.Net.Http; using System.Net.Http;
using ReC.Client.Api; using ReC.Client.Api;
@@ -56,16 +58,23 @@ namespace ReC.Client
/// Initializes a new instance of the <see cref="ReCClient"/> class. /// Initializes a new instance of the <see cref="ReCClient"/> class.
/// </summary> /// </summary>
/// <param name="httpClientFactory">The factory to create HttpClients.</param> /// <param name="httpClientFactory">The factory to create HttpClients.</param>
public ReCClient(IHttpClientFactory httpClientFactory) /// <param name="options">An optional set of client options. Defaults are used when omitted.</param>
/// <param name="logger">An optional logger used to record API call outcomes.</param>
#if NETFRAMEWORK
public ReCClient(IHttpClientFactory httpClientFactory, IOptions<ReCClientOptions> options = null, ILogger logger = null)
#else
public ReCClient(IHttpClientFactory httpClientFactory, IOptions<ReCClientOptions>? options = null, ILogger<ReCClient>? logger = null)
#endif
{ {
_http = httpClientFactory.CreateClient(ClientName); _http = httpClientFactory.CreateClient(ClientName);
RecActions = new RecActionApi(_http); var opts = options?.Value ?? new ReCClientOptions();
Results = new ResultApi(_http); RecActions = new RecActionApi(_http, logger, opts);
Profiles = new ProfileApi(_http); Results = new ResultApi(_http, logger, opts);
EndpointAuth = new EndpointAuthApi(_http); Profiles = new ProfileApi(_http, logger, opts);
EndpointParams = new EndpointParamsApi(_http); EndpointAuth = new EndpointAuthApi(_http, logger, opts);
Endpoints = new EndpointsApi(_http); EndpointParams = new EndpointParamsApi(_http, logger, opts);
Common = new CommonApi(_http); Endpoints = new EndpointsApi(_http, logger, opts);
Common = new CommonApi(_http, logger, opts);
} }
#region Static #region Static

View File

@@ -1,11 +1,11 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net.Http.Json;
#if NETFRAMEWORK
using System.Net.Http; using System.Net.Http;
#endif using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace ReC.Client namespace ReC.Client
{ {
@@ -45,5 +45,67 @@ namespace ReC.Client
/// <param name="payload">The payload to serialize.</param> /// <param name="payload">The payload to serialize.</param>
/// <returns>A <see cref="JsonContent"/> instance ready for HTTP requests.</returns> /// <returns>A <see cref="JsonContent"/> instance ready for HTTP requests.</returns>
public static JsonContent ToJsonContent<T>(T payload) => JsonContent.Create(payload); public static JsonContent ToJsonContent<T>(T payload) => JsonContent.Create(payload);
/// <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);
}
} }
} }

View File

@@ -0,0 +1,16 @@
namespace ReC.Client
{
/// <summary>
/// Options that control the behavior of the <see cref="ReCClient"/>.
/// </summary>
public class ReCClientOptions
{
/// <summary>
/// Gets or sets a value indicating whether successful API requests should be
/// logged through the injected <see cref="Microsoft.Extensions.Logging.ILogger"/>.
/// Failed requests always throw <see cref="ReCApiException"/> regardless of this setting.
/// Defaults to <see langword="true"/>.
/// </summary>
public bool LogSuccessfulRequests { get; set; } = true;
}
}