Compare commits
20 Commits
761fd208e5
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d8e51ad70 | |||
| 01ac7ece1e | |||
| e0c2aab2b1 | |||
| a43d1ebc20 | |||
| f96ad1ac7e | |||
| 20766091a9 | |||
| 91c166dc4d | |||
| 7ed348832c | |||
| 190d41489e | |||
| dfcf1fb536 | |||
| 136c2fcb30 | |||
| 71defc0e4c | |||
| 992395dec3 | |||
| 82ec333f23 | |||
| a924e32291 | |||
| 28a4146069 | |||
| 17d40817f2 | |||
| 330443d2c9 | |||
| 6ca876c762 | |||
| e89af1cbcd |
@@ -48,10 +48,13 @@ try
|
||||
?? throw new InvalidOperationException("Connection string is not found.");
|
||||
|
||||
var logger = provider.GetRequiredService<ILogger<RecDbContext>>();
|
||||
var enableSensitiveDataLogging = config.GetValue("EfCore:EnableSensitiveDataLogging", true);
|
||||
var enableDetailedErrors = config.GetValue("EfCore:EnableDetailedErrors", false);
|
||||
|
||||
opt.UseSqlServer(cnnStr)
|
||||
.LogTo(log => logger.LogInformation("{log}", log), LogLevel.Trace)
|
||||
.EnableSensitiveDataLogging()
|
||||
.EnableDetailedErrors();
|
||||
.EnableSensitiveDataLogging(enableSensitiveDataLogging)
|
||||
.EnableDetailedErrors(enableDetailedErrors);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
<Product>ReC.API</Product>
|
||||
<PackageIcon>Assets\icon.ico</PackageIcon>
|
||||
<PackageTags>digital data rest-caller rec api</PackageTags>
|
||||
<Version>2.2.1-beta</Version>
|
||||
<AssemblyVersion>2.2.1.0</AssemblyVersion>
|
||||
<FileVersion>2.2.1.0</FileVersion>
|
||||
<InformationalVersion>2.2.0-beta</InformationalVersion>
|
||||
<Version>2.4.0-beta</Version>
|
||||
<AssemblyVersion>2.4.0.0</AssemblyVersion>
|
||||
<FileVersion>2.4.0.0</FileVersion>
|
||||
<InformationalVersion>2.4.0-beta</InformationalVersion>
|
||||
<Copyright>Copyright © 2025 Digital Data GmbH. All rights reserved.</Copyright>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
|
||||
@@ -5,9 +5,13 @@
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"LuckyPennySoftwareLicenseKey": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx1Y2t5UGVubnlTb2Z0d2FyZUxpY2Vuc2VLZXkvYmJiMTNhY2I1OTkwNGQ4OWI0Y2IxYzg1ZjA4OGNjZjkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2x1Y2t5cGVubnlzb2Z0d2FyZS5jb20iLCJhdWQiOiJMdWNreVBlbm55U29mdHdhcmUiLCJleHAiOiIxNzg0ODUxMjAwIiwiaWF0IjoiMTc1MzM2MjQ5MSIsImFjY291bnRfaWQiOiIwMTk4M2M1OWU0YjM3MjhlYmZkMzEwM2MyYTQ4NmU4NSIsImN1c3RvbWVyX2lkIjoiY3RtXzAxazB5NmV3MmQ4YTk4Mzg3aDJnbTRuOWswIiwic3ViX2lkIjoiLSIsImVkaXRpb24iOiIwIiwidHlwZSI6IjIifQ.ZqsFG7kv_-xGfxS6ACk3i0iuNiVUXX2AvPI8iAcZ6-z2170lGv__aO32tWpQccD9LCv5931lBNLWSblKS0MT3gOt-5he2TEftwiSQGFwoIBgtOHWsNRMinUrg2trceSp3IhyS3UaMwnxZDrCvx4-0O-kpOzVpizeHUAZNr5U7oSCWO34bpKdae6grtM5e3f93Z1vs7BW_iPgItd-aLvPwApbaG9VhmBTKlQ7b4Jh64y7UXJ9mKP7Qb_Oa97oEg0oY5DPHOWTZWeE1EzORgVr2qkK2DELSHuZ_EIUhODojkClPNAKtvEl_qEjpq0HZCIvGwfCCRlKlSkQqIeZdFkiXg",
|
||||
"EfCore": {
|
||||
"EnableSensitiveDataLogging": true,
|
||||
"EnableDetailedErrors": false
|
||||
},
|
||||
"RecAction": {
|
||||
"AddedWho": "ReC.API",
|
||||
"UseHttp1ForNtlm": false
|
||||
"UseHttp1ForNtlm": false,
|
||||
"AutoDetectHeaders": false
|
||||
},
|
||||
// Bad request SqlException numbers numbers can be updated at runtime; no restart required.
|
||||
"SqlException": {
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
public class RecActionOptions
|
||||
{
|
||||
public bool UseHttp1ForNtlm { get; set; } = false;
|
||||
public bool AutoDetectHeaders { get; set; } = false;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ReC.Application.Common.Constants;
|
||||
using ReC.Application.Common.Dto;
|
||||
@@ -35,7 +36,8 @@ public class InvokeRecActionViewCommandHandler(
|
||||
IOptions<RecActionOptions> options,
|
||||
ISender sender,
|
||||
IHttpClientFactory clientFactory,
|
||||
IConfiguration? config = null
|
||||
IConfiguration? config = null,
|
||||
ILogger<InvokeRecActionViewCommandHandler>? logger = null
|
||||
) : IRequestHandler<InvokeRecActionViewCommand>
|
||||
{
|
||||
private readonly RecActionOptions _options = options.Value;
|
||||
@@ -57,11 +59,47 @@ public class InvokeRecActionViewCommandHandler(
|
||||
using var httpReq = CreateHttpRequestMessage(restType, action.EndpointUri);
|
||||
|
||||
if (action.Body is not null)
|
||||
{
|
||||
httpReq.Content = new StringContent(action.Body);
|
||||
|
||||
var contentType = action.Headers?.FirstOrDefault(h => h.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase));
|
||||
if (contentType is not null && !string.IsNullOrWhiteSpace(contentType.Value.Value))
|
||||
try { httpReq.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType.Value.Value); }
|
||||
catch (FormatException ex)
|
||||
{
|
||||
logger?.LogWarning(ex, "Content-Type '{Value}' could not be parsed with strict validation, falling back to TryAddWithoutValidation. ActionId: {ActionId}, ProfileId: {ProfileId}", contentType.Value.Value, action.Id, action.ProfileId);
|
||||
httpReq.Content.Headers.TryAddWithoutValidation("Content-Type", contentType.Value.Value);
|
||||
}
|
||||
else if (_options.AutoDetectHeaders)
|
||||
{
|
||||
var body = action.Body.TrimStart();
|
||||
if (body.StartsWith('{') || body.StartsWith('['))
|
||||
{
|
||||
httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json") { CharSet = "utf-8" };
|
||||
logger?.LogWarning("Content-Type header was not specified. Auto-detected 'application/json; charset=utf-8' based on body content. ActionId: {ActionId}, ProfileId: {ProfileId}", action.Id, action.ProfileId);
|
||||
}
|
||||
else if (body.StartsWith('<'))
|
||||
{
|
||||
httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue("application/xml") { CharSet = "utf-8" };
|
||||
logger?.LogWarning("Content-Type header was not specified. Auto-detected 'application/xml; charset=utf-8' based on body content. ActionId: {ActionId}, ProfileId: {ProfileId}", action.Id, action.ProfileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (action.Headers is not null)
|
||||
foreach (var header in action.Headers)
|
||||
httpReq.Headers.Add(header.Key, header.Value);
|
||||
foreach (var header in action.Headers.Where(h => !h.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase)))
|
||||
try { httpReq.Headers.Add(header.Key, header.Value); }
|
||||
catch (FormatException ex)
|
||||
{
|
||||
logger?.LogWarning(ex, "Header '{Key}' could not be added with strict validation, falling back to TryAddWithoutValidation. ActionId: {ActionId}, ProfileId: {ProfileId}", header.Key, action.Id, action.ProfileId);
|
||||
httpReq.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
|
||||
if (_options.AutoDetectHeaders && !httpReq.Headers.Contains("Accept"))
|
||||
{
|
||||
httpReq.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
logger?.LogWarning("Accept header was not specified. Defaulting to 'application/json'. ActionId: {ActionId}, ProfileId: {ProfileId}", action.Id, action.ProfileId);
|
||||
}
|
||||
|
||||
switch (action.EndpointAuthType)
|
||||
{
|
||||
@@ -74,7 +112,12 @@ public class InvokeRecActionViewCommandHandler(
|
||||
switch (action.EndpointAuthApiKeyAddTo)
|
||||
{
|
||||
case ApiKeyLocation.Header:
|
||||
httpReq.Headers.Add(apiKey, apiValue);
|
||||
try { httpReq.Headers.Add(apiKey, apiValue); }
|
||||
catch (FormatException ex)
|
||||
{
|
||||
logger?.LogWarning(ex, "ApiKey header '{Key}' could not be added with strict validation, falling back to TryAddWithoutValidation. ActionId: {ActionId}, ProfileId: {ProfileId}", apiKey, action.Id, action.ProfileId);
|
||||
httpReq.Headers.TryAddWithoutValidation(apiKey, apiValue);
|
||||
}
|
||||
break;
|
||||
case ApiKeyLocation.Query:
|
||||
var uriBuilder = new UriBuilder(httpReq.RequestUri!);
|
||||
@@ -97,14 +140,24 @@ public class InvokeRecActionViewCommandHandler(
|
||||
case EndpointAuthType.JwtBearer:
|
||||
case EndpointAuthType.OAuth2:
|
||||
if (action.EndpointAuthToken is string authToken)
|
||||
httpReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
|
||||
try { httpReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken); }
|
||||
catch (FormatException ex)
|
||||
{
|
||||
logger?.LogWarning(ex, "Bearer token could not be set with strict validation, falling back to TryAddWithoutValidation. ActionId: {ActionId}, ProfileId: {ProfileId}", action.Id, action.ProfileId);
|
||||
httpReq.Headers.TryAddWithoutValidation("Authorization", $"Bearer {authToken}");
|
||||
}
|
||||
break;
|
||||
|
||||
case EndpointAuthType.BasicAuth:
|
||||
if (action.EndpointAuthUsername is string authUsername && action.EndpointAuthPassword is string authPassword)
|
||||
{
|
||||
var basicAuth = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{authUsername}:{authPassword}"));
|
||||
httpReq.Headers.Authorization = new AuthenticationHeaderValue("Basic", basicAuth);
|
||||
try { httpReq.Headers.Authorization = new AuthenticationHeaderValue("Basic", basicAuth); }
|
||||
catch (FormatException ex)
|
||||
{
|
||||
logger?.LogWarning(ex, "Basic auth could not be set with strict validation, falling back to TryAddWithoutValidation. ActionId: {ActionId}, ProfileId: {ProfileId}", action.Id, action.ProfileId);
|
||||
httpReq.Headers.TryAddWithoutValidation("Authorization", $"Basic {basicAuth}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ReC.Client.Api
|
||||
{
|
||||
@@ -20,15 +21,37 @@ namespace ReC.Client.Api
|
||||
/// </summary>
|
||||
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>
|
||||
/// Initializes a new instance of the <see cref="BaseCrudApi"/> class.
|
||||
/// </summary>
|
||||
/// <param name="http">The HTTP client used for requests.</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));
|
||||
ResourcePath = resourcePath ?? throw new ArgumentNullException(nameof(resourcePath));
|
||||
Logger = logger;
|
||||
Options = options ?? new ReCClientOptions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -37,9 +60,15 @@ namespace ReC.Client.Api
|
||||
/// <typeparam name="T">The payload type.</typeparam>
|
||||
/// <param name="payload">The payload to send.</param>
|
||||
/// <param name="cancel">A token to cancel the operation.</param>
|
||||
/// <returns>The HTTP response message.</returns>
|
||||
public Task<HttpResponseMessage> CreateAsync<T>(T payload, CancellationToken cancel = default)
|
||||
=> Http.PostAsync(ResourcePath, ReCClientHelpers.ToJsonContent(payload), cancel);
|
||||
/// <exception cref="ReCApiException">Thrown when the API responds with a non-success status code.</exception>
|
||||
public async Task CreateAsync<T>(T payload, CancellationToken cancel = default)
|
||||
{
|
||||
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>
|
||||
/// Updates a resource by identifier.
|
||||
@@ -48,9 +77,15 @@ namespace ReC.Client.Api
|
||||
/// <param name="id">The resource identifier.</param>
|
||||
/// <param name="payload">The payload to send.</param>
|
||||
/// <param name="cancel">A token to cancel the operation.</param>
|
||||
/// <returns>The HTTP response message.</returns>
|
||||
public Task<HttpResponseMessage> UpdateAsync<T>(long id, T payload, CancellationToken cancel = default)
|
||||
=> Http.PutAsync($"{ResourcePath}/{id}", ReCClientHelpers.ToJsonContent(payload), cancel);
|
||||
/// <exception cref="ReCApiException">Thrown when the API responds with a non-success status code.</exception>
|
||||
public async Task UpdateAsync<T>(long id, T payload, CancellationToken cancel = default)
|
||||
{
|
||||
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>
|
||||
/// 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>
|
||||
/// <param name="payload">The payload to send.</param>
|
||||
/// <param name="cancel">A token to cancel the operation.</param>
|
||||
/// <returns>The HTTP response message.</returns>
|
||||
public Task<HttpResponseMessage> DeleteAsync<T>(T payload, CancellationToken cancel = default)
|
||||
/// <exception cref="ReCApiException">Thrown when the API responds with a non-success status code.</exception>
|
||||
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)
|
||||
};
|
||||
return Http.SendAsync(request, cancel);
|
||||
})
|
||||
using (var resp = await Http.SendAsync(request, cancel))
|
||||
{
|
||||
await ReCClientHelpers.HandleResponseAsync(resp, Logger, Options.LogSuccessfulRequests, cancel).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ReC.Client.Api
|
||||
{
|
||||
@@ -13,7 +14,13 @@ namespace ReC.Client.Api
|
||||
/// Initializes a new instance of the <see cref="CommonApi"/> class.
|
||||
/// </summary>
|
||||
/// <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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ReC.Client.Api
|
||||
{
|
||||
@@ -13,7 +14,13 @@ namespace ReC.Client.Api
|
||||
/// Initializes a new instance of the <see cref="EndpointAuthApi"/> class.
|
||||
/// </summary>
|
||||
/// <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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ReC.Client.Api
|
||||
{
|
||||
@@ -13,7 +14,13 @@ namespace ReC.Client.Api
|
||||
/// Initializes a new instance of the <see cref="EndpointParamsApi"/> class.
|
||||
/// </summary>
|
||||
/// <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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ReC.Client.Api
|
||||
{
|
||||
@@ -13,7 +14,13 @@ namespace ReC.Client.Api
|
||||
/// Initializes a new instance of the <see cref="EndpointsApi"/> class.
|
||||
/// </summary>
|
||||
/// <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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ReC.Client.Api
|
||||
{
|
||||
@@ -13,7 +14,13 @@ namespace ReC.Client.Api
|
||||
/// Initializes a new instance of the <see cref="ProfileApi"/> class.
|
||||
/// </summary>
|
||||
/// <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
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ReC.Client.Api
|
||||
{
|
||||
@@ -13,7 +14,13 @@ namespace ReC.Client.Api
|
||||
/// Initializes a new instance of the <see cref="RecActionApi"/> class.
|
||||
/// </summary>
|
||||
/// <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="references">Optional reference values to pass through to all result records.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns><see langword="true"/> if the request succeeds; otherwise, <see langword="false"/>.</returns>
|
||||
public async Task<bool> InvokeAsync(int profileId, InvokeReferences references, CancellationToken cancellationToken = default)
|
||||
/// <exception cref="ReCApiException">Thrown when the API responds with a non-success status code.</exception>
|
||||
public async Task InvokeAsync(int profileId, InvokeReferences references, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var content = references != null ? ReCClientHelpers.ToJsonContent(references) : null;
|
||||
var resp = await Http.PostAsync($"{ResourcePath}/invoke/{profileId}", content, cancellationToken);
|
||||
return resp.IsSuccessStatusCode;
|
||||
using (content)
|
||||
using (var resp = await Http.PostAsync($"{ResourcePath}/invoke/{profileId}", content, cancellationToken))
|
||||
{
|
||||
await ReCClientHelpers.HandleResponseAsync(resp, Logger, Options.LogSuccessfulRequests, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -37,8 +47,8 @@ namespace ReC.Client.Api
|
||||
/// <param name="profileId">The profile identifier.</param>
|
||||
/// <param name="batchId">Batch identifier.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns><see langword="true"/> if the request succeeds; otherwise, <see langword="false"/>.</returns>
|
||||
public Task<bool> InvokeAsync(int profileId, string batchId, CancellationToken cancellationToken = default)
|
||||
/// <exception cref="ReCApiException">Thrown when the API responds with a non-success status code.</exception>
|
||||
public Task InvokeAsync(int profileId, string batchId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return InvokeAsync(profileId, new InvokeReferences() { BatchId = batchId }, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ReC.Client.Api
|
||||
{
|
||||
@@ -13,7 +14,13 @@ namespace ReC.Client.Api
|
||||
/// Initializes a new instance of the <see cref="ResultApi"/> class.
|
||||
/// </summary>
|
||||
/// <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
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
#if NETFRAMEWORK
|
||||
using System.Net.Http;
|
||||
#endif
|
||||
|
||||
@@ -16,9 +16,15 @@ namespace ReC.Client
|
||||
/// </summary>
|
||||
/// <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="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>
|
||||
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>();
|
||||
return services.AddHttpClient(ReCClient.ClientName, client =>
|
||||
{
|
||||
@@ -31,11 +37,29 @@ namespace ReC.Client
|
||||
/// </summary>
|
||||
/// <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="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>
|
||||
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>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src/ReC.Client/ReCApiException.cs
Normal file
88
src/ReC.Client/ReCApiException.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using ReC.Client.Api;
|
||||
@@ -56,16 +58,23 @@ namespace ReC.Client
|
||||
/// Initializes a new instance of the <see cref="ReCClient"/> class.
|
||||
/// </summary>
|
||||
/// <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);
|
||||
RecActions = new RecActionApi(_http);
|
||||
Results = new ResultApi(_http);
|
||||
Profiles = new ProfileApi(_http);
|
||||
EndpointAuth = new EndpointAuthApi(_http);
|
||||
EndpointParams = new EndpointParamsApi(_http);
|
||||
Endpoints = new EndpointsApi(_http);
|
||||
Common = new CommonApi(_http);
|
||||
var opts = options?.Value ?? new ReCClientOptions();
|
||||
RecActions = new RecActionApi(_http, logger, opts);
|
||||
Results = new ResultApi(_http, logger, opts);
|
||||
Profiles = new ProfileApi(_http, logger, opts);
|
||||
EndpointAuth = new EndpointAuthApi(_http, logger, opts);
|
||||
EndpointParams = new EndpointParamsApi(_http, logger, opts);
|
||||
Endpoints = new EndpointsApi(_http, logger, opts);
|
||||
Common = new CommonApi(_http, logger, opts);
|
||||
}
|
||||
|
||||
#region Static
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
#if NETFRAMEWORK
|
||||
using System.Net.Http;
|
||||
#endif
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ReC.Client
|
||||
{
|
||||
@@ -45,5 +45,67 @@ namespace ReC.Client
|
||||
/// <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>
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
src/ReC.Client/ReCClientOptions.cs
Normal file
16
src/ReC.Client/ReCClientOptions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user