Refactor HTTP client management and service lifetimes

Updated DependencyInjection.cs to change ISmsSender and
IEnvelopeSmsHandler lifetimes from Singleton to Scoped,
ensuring per-request instantiation. Added Microsoft.Extensions.Http
package to EnvelopeGenerator.Server.Client.csproj for enhanced
HttpClient handling. Refactored AnnotationService, AuthService,
DocumentService, EnvelopeReceiverService, SignatureCacheService,
and SignatureService to use IHttpClientFactory, improving
flexibility and testability. Introduced a named HttpClient
"EnvelopeGenerator.Server" in Program.cs for internal API calls,
and removed the previous HttpClient setup using HttpContextAccessor.
Added necessary using directives for System.Net.Http across
service files to support these changes.
This commit is contained in:
2026-06-22 17:35:00 +02:00
parent 106e62a912
commit b6ec5307b6
9 changed files with 39 additions and 33 deletions

View File

@@ -51,8 +51,8 @@ public static class DependencyInjection
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams))); services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams))); services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
services.TryAddSingleton<ISmsSender, GTXSmsSender>(); services.TryAddScoped<ISmsSender, GTXSmsSender>(); // Changed: Singleton → Scoped
services.TryAddSingleton<IEnvelopeSmsHandler, EnvelopeSmsHandler>(); services.TryAddScoped<IEnvelopeSmsHandler, EnvelopeSmsHandler>(); // Changed: Singleton → Scoped
services.TryAddSingleton<IAuthenticator, Authenticator>(); services.TryAddSingleton<IAuthenticator, Authenticator>();
services.TryAddSingleton<QRCodeGenerator>(); services.TryAddSingleton<QRCodeGenerator>();

View File

@@ -17,6 +17,7 @@
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" /> <PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.3" /> <PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.3" />
<PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.2" /> <PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.9" />
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="3.119.1" /> <PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="3.119.1" />
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.1" /> <PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.1" />
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\*.a" /> <NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\*.a" />

View File

@@ -1,3 +1,4 @@
using System.Net.Http;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using EnvelopeGenerator.Server.Client.Models; using EnvelopeGenerator.Server.Client.Models;
@@ -15,13 +16,14 @@ namespace EnvelopeGenerator.Server.Client.Services;
/// YARP route in <c>yarp.json</c> — no code change required. /// YARP route in <c>yarp.json</c> — no code change required.
/// </summary> /// </summary>
[Obsolete("Use SignatureService.")] [Obsolete("Use SignatureService.")]
public class AnnotationService(HttpClient http, IOptions<ApiOptions> apiOptions) public class AnnotationService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
{ {
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web); private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
public async Task<IReadOnlyList<AnnotationDto>> GetAnnotationsAsync(string envelopeKey, CancellationToken cancel = default) public async Task<IReadOnlyList<AnnotationDto>> GetAnnotationsAsync(string envelopeKey, CancellationToken cancel = default)
{ {
var url = $"{apiOptions.Value.BaseUrl}/api/Annotation/{Uri.EscapeDataString(envelopeKey)}"; using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var url = $"/api/Annotation/{Uri.EscapeDataString(envelopeKey)}";
var response = await http.GetAsync(url, cancel); var response = await http.GetAsync(url, cancel);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)

View File

@@ -1,4 +1,5 @@
using System.Net; using System.Net;
using System.Net.Http;
using System.Net.Http.Json; using System.Net.Http.Json;
using EnvelopeGenerator.Server.Client.Options; using EnvelopeGenerator.Server.Client.Options;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@@ -9,7 +10,7 @@ public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error }
public enum SenderLoginResult { Success, InvalidCredentials, Error } public enum SenderLoginResult { Success, InvalidCredentials, Error }
public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions) public class AuthService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
{ {
private readonly ApiOptions _api = apiOptions.Value; private readonly ApiOptions _api = apiOptions.Value;
@@ -19,7 +20,8 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
/// </summary> /// </summary>
public async Task<bool> CheckEnvelopeAccessAsync(string envelopeKey, CancellationToken cancel = default) public async Task<bool> CheckEnvelopeAccessAsync(string envelopeKey, CancellationToken cancel = default)
{ {
var response = await http.GetAsync($"{_api.BaseUrl}/api/auth/check/envelope/{Uri.EscapeDataString(envelopeKey)}", cancel); using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var response = await http.GetAsync($"/api/auth/check/envelope/{Uri.EscapeDataString(envelopeKey)}", cancel);
return response.StatusCode == HttpStatusCode.OK; return response.StatusCode == HttpStatusCode.OK;
} }
@@ -30,11 +32,12 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
/// </summary> /// </summary>
public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string envelopeKey, string accessCode, CancellationToken cancel = default) public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string envelopeKey, string accessCode, CancellationToken cancel = default)
{ {
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var form = new MultipartFormDataContent(); var form = new MultipartFormDataContent();
form.Add(new StringContent(accessCode), "AccessCode"); form.Add(new StringContent(accessCode), "AccessCode");
var response = await http.PostAsync( var response = await http.PostAsync(
$"{_api.BaseUrl}/api/Auth/envelope-receiver/{Uri.EscapeDataString(envelopeKey)}", $"/api/Auth/envelope-receiver/{Uri.EscapeDataString(envelopeKey)}",
form, cancel); form, cancel);
return response.StatusCode switch return response.StatusCode switch
@@ -52,8 +55,9 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
/// </summary> /// </summary>
public async Task<bool> LogoutEnvelopeReceiverAsync(string envelopeKey, CancellationToken cancel = default) public async Task<bool> LogoutEnvelopeReceiverAsync(string envelopeKey, CancellationToken cancel = default)
{ {
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var response = await http.PostAsync( var response = await http.PostAsync(
$"{_api.BaseUrl}/api/auth/logout/envelope/{Uri.EscapeDataString(envelopeKey)}", $"/api/auth/logout/envelope/{Uri.EscapeDataString(envelopeKey)}",
null, cancel); null, cancel);
return response.IsSuccessStatusCode; return response.IsSuccessStatusCode;
} }
@@ -65,10 +69,11 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
/// </summary> /// </summary>
public async Task<SenderLoginResult> LoginSenderAsync(string username, string password, CancellationToken cancel = default) public async Task<SenderLoginResult> LoginSenderAsync(string username, string password, CancellationToken cancel = default)
{ {
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var requestBody = new { username, password }; var requestBody = new { username, password };
var response = await http.PostAsJsonAsync( var response = await http.PostAsJsonAsync(
$"{_api.BaseUrl}/api/auth?cookie=true", $"/api/auth?cookie=true",
requestBody, cancel); requestBody, cancel);
return response.StatusCode switch return response.StatusCode switch

View File

@@ -5,7 +5,7 @@ using EnvelopeGenerator.Server.Client.Options;
namespace EnvelopeGenerator.Server.Client.Services; namespace EnvelopeGenerator.Server.Client.Services;
public class DocumentService(HttpClient http, IOptions<ApiOptions> apiOptions) public class DocumentService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
{ {
private readonly ApiOptions _api = apiOptions.Value; private readonly ApiOptions _api = apiOptions.Value;
@@ -16,7 +16,8 @@ public class DocumentService(HttpClient http, IOptions<ApiOptions> apiOptions)
/// <exception cref="HttpRequestException">Thrown when the API request fails.</exception> /// <exception cref="HttpRequestException">Thrown when the API request fails.</exception>
public async Task<byte[]?> GetDocumentAsync(string envelopeKey, CancellationToken cancel = default) public async Task<byte[]?> GetDocumentAsync(string envelopeKey, CancellationToken cancel = default)
{ {
var response = await http.GetAsync($"{_api.BaseUrl}/api/Document/{Uri.EscapeDataString(envelopeKey)}", cancel); using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var response = await http.GetAsync($"/api/Document/{Uri.EscapeDataString(envelopeKey)}", cancel);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {

View File

@@ -1,4 +1,5 @@
using System.Net; using System.Net;
using System.Net.Http;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using EnvelopeGenerator.Server.Client.Models; using EnvelopeGenerator.Server.Client.Models;
@@ -11,7 +12,7 @@ namespace EnvelopeGenerator.Server.Client.Services;
/// Retrieves the <see cref="EnvelopeReceiverDto"/> for the authenticated receiver /// Retrieves the <see cref="EnvelopeReceiverDto"/> for the authenticated receiver
/// from <c>GET api/EnvelopeReceiver/{envelopeKey}</c>. /// from <c>GET api/EnvelopeReceiver/{envelopeKey}</c>.
/// </summary> /// </summary>
public class EnvelopeReceiverService(HttpClient http, IOptions<ApiOptions> apiOptions) public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
{ {
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web); private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
@@ -22,7 +23,8 @@ public class EnvelopeReceiverService(HttpClient http, IOptions<ApiOptions> apiOp
/// <exception cref="HttpRequestException">Thrown when the API request fails.</exception> /// <exception cref="HttpRequestException">Thrown when the API request fails.</exception>
public async Task<EnvelopeReceiverDto?> GetAsync(string envelopeKey, CancellationToken cancel = default) public async Task<EnvelopeReceiverDto?> GetAsync(string envelopeKey, CancellationToken cancel = default)
{ {
var url = $"{apiOptions.Value.BaseUrl}/api/EnvelopeReceiver/{Uri.EscapeDataString(envelopeKey)}"; using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var url = $"/api/EnvelopeReceiver/{Uri.EscapeDataString(envelopeKey)}";
var response = await http.GetAsync(url, cancel); var response = await http.GetAsync(url, cancel);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)

View File

@@ -1,3 +1,4 @@
using System.Net.Http;
using System.Net.Http.Json; using System.Net.Http.Json;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using EnvelopeGenerator.Server.Client.Options; using EnvelopeGenerator.Server.Client.Options;
@@ -8,7 +9,7 @@ namespace EnvelopeGenerator.Server.Client.Services;
/// <summary> /// <summary>
/// Client service for managing cached signatures via API. /// Client service for managing cached signatures via API.
/// </summary> /// </summary>
public class SignatureCacheService(HttpClient http, IOptions<ApiOptions> apiOptions) public class SignatureCacheService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
{ {
private readonly ApiOptions _api = apiOptions.Value; private readonly ApiOptions _api = apiOptions.Value;
@@ -17,8 +18,9 @@ public class SignatureCacheService(HttpClient http, IOptions<ApiOptions> apiOpti
SignatureCaptureDto signature, SignatureCaptureDto signature,
CancellationToken cancel = default) CancellationToken cancel = default)
{ {
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var response = await http.PostAsJsonAsync( var response = await http.PostAsJsonAsync(
$"{_api.BaseUrl}/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}", $"/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
signature, signature,
cancel); cancel);
@@ -33,8 +35,9 @@ public class SignatureCacheService(HttpClient http, IOptions<ApiOptions> apiOpti
string envelopeKey, string envelopeKey,
CancellationToken cancel = default) CancellationToken cancel = default)
{ {
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var response = await http.GetAsync( var response = await http.GetAsync(
$"{_api.BaseUrl}/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}", $"/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
cancel); cancel);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound) if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
@@ -53,8 +56,9 @@ public class SignatureCacheService(HttpClient http, IOptions<ApiOptions> apiOpti
string envelopeKey, string envelopeKey,
CancellationToken cancel = default) CancellationToken cancel = default)
{ {
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var response = await http.DeleteAsync( var response = await http.DeleteAsync(
$"{_api.BaseUrl}/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}", $"/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
cancel); cancel);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)

View File

@@ -1,3 +1,4 @@
using System.Net.Http;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using EnvelopeGenerator.Server.Client.Models; using EnvelopeGenerator.Server.Client.Models;
@@ -6,13 +7,14 @@ using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.Server.Client.Services; namespace EnvelopeGenerator.Server.Client.Services;
public class SignatureService(HttpClient http, IOptions<ApiOptions> apiOptions) public class SignatureService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
{ {
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web); private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
public async Task<IReadOnlyList<SignatureDto>> GetAsync(string envelopeKey, CancellationToken cancel = default) public async Task<IReadOnlyList<SignatureDto>> GetAsync(string envelopeKey, CancellationToken cancel = default)
{ {
var url = $"{apiOptions.Value.BaseUrl}/api/Signature/{Uri.EscapeDataString(envelopeKey)}"; using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var url = $"/api/Signature/{Uri.EscapeDataString(envelopeKey)}";
var response = await http.GetAsync(url, cancel); var response = await http.GetAsync(url, cancel);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)

View File

@@ -53,6 +53,9 @@ try
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddHttpClient(); builder.Services.AddHttpClient();
// Named HttpClient for internal API calls (same domain, uses relative paths)
builder.Services.AddHttpClient("EnvelopeGenerator.Server");
// CORS Policy // CORS Policy
var allowedOrigins = config.GetSection("AllowedOrigins").Get<string[]>() ?? var allowedOrigins = config.GetSection("AllowedOrigins").Get<string[]>() ??
throw new InvalidOperationException("AllowedOrigins section is missing in the configuration."); throw new InvalidOperationException("AllowedOrigins section is missing in the configuration.");
@@ -289,20 +292,6 @@ try
// HttpClient for server-side components (e.g., MainLayout with FontLoader) // HttpClient for server-side components (e.g., MainLayout with FontLoader)
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<HttpClient>(sp =>
{
var httpContextAccessor = sp.GetRequiredService<IHttpContextAccessor>();
var request = httpContextAccessor.HttpContext?.Request;
var httpClient = sp.GetRequiredService<IHttpClientFactory>().CreateClient();
if (request != null)
{
httpClient.BaseAddress = new Uri($"{request.Scheme}://{request.Host}");
}
return httpClient;
});
// Business Services (Server specific) // Business Services (Server specific)
builder.Services.AddScoped<DocumentService>(); builder.Services.AddScoped<DocumentService>();