133 lines
5.4 KiB
C#
133 lines
5.4 KiB
C#
using System.Net.Http.Headers;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Net.Http.Json;
|
|
using Jugenddienst_Stunden.Interfaces;
|
|
|
|
namespace Jugenddienst_Stunden.Infrastructure;
|
|
|
|
internal sealed class ApiClient : IApiClient {
|
|
private readonly HttpClient _http;
|
|
private readonly JsonSerializerOptions _json;
|
|
private readonly ApiOptions _options;
|
|
private readonly IAppSettings _settings;
|
|
|
|
public ApiClient(HttpClient http, ApiOptions options, ITokenProvider tokenProvider, IAppSettings settings) {
|
|
_http = http;
|
|
_options = options;
|
|
_settings = settings;
|
|
|
|
_http.Timeout = options.Timeout;
|
|
if (!_http.DefaultRequestHeaders.Accept.Any())
|
|
_http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
|
|
|
var token = tokenProvider.GetToken();
|
|
if (!string.IsNullOrWhiteSpace(token))
|
|
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
|
|
|
_json = new JsonSerializerOptions {
|
|
PropertyNameCaseInsensitive = true,
|
|
WriteIndented = false,
|
|
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
|
};
|
|
|
|
// Globale Converter: erlauben numerische Felder auch als Strings (z.B. user.id)
|
|
_json.Converters.Add(new Jugenddienst_Stunden.Models.JsonFlexibleInt32Converter());
|
|
_json.Converters.Add(new Jugenddienst_Stunden.Models.JsonFlexibleNullableInt32Converter());
|
|
|
|
// Stelle sicher, dass die BaseAddress sofort aus den aktuellen Settings (Preferences) gesetzt wird
|
|
// und nicht erst beim ersten Request. Dadurch steht die ApiUrl ab Initialisierung zur Verfügung.
|
|
EnsureBaseAddress();
|
|
}
|
|
|
|
public Task<T> GetAsync<T>(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default)
|
|
=> SendAsync<T>(HttpMethod.Get, path, null, query, ct);
|
|
|
|
public async Task<T> SendAsync<T>(HttpMethod method, string path, object? body = null,
|
|
IDictionary<string, string?>? query = null, CancellationToken ct = default) {
|
|
// Vor jedem Request sicherstellen, dass die (ggf. geänderte) BaseAddress gesetzt ist
|
|
EnsureBaseAddress();
|
|
var uri = BuildUri(path, query);
|
|
using var req = new HttpRequestMessage(method, uri) {
|
|
Content = body is null ? null : JsonContent.Create(body, options: _json)
|
|
};
|
|
|
|
using var res = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
|
|
var text = await res.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
|
|
|
//if (!res.IsSuccessStatusCode)
|
|
// throw ApiException.From(res.StatusCode, TryGetMessage(text), text);
|
|
|
|
if (res.StatusCode != System.Net.HttpStatusCode.OK) {
|
|
// Verhalten wie in BaseFunc: bei Fehlerstatus -> "message" aus Body lesen und mit dessen Inhalt eine Exception werfen.
|
|
try {
|
|
var options = new JsonDocumentOptions { AllowTrailingCommas = true };
|
|
using var doc = JsonDocument.Parse(text, options);
|
|
var root = doc.RootElement;
|
|
// GetProperty wirft, wenn "message" fehlt — das entspricht dem bisherigen Verhalten in BaseFunc.
|
|
var messageElement = root.GetProperty("message");
|
|
if (messageElement.ValueKind != JsonValueKind.String)
|
|
throw ApiException.From(res.StatusCode, "Fehler: 'message' ist null.", text);
|
|
|
|
var message = messageElement.GetString() ?? throw ApiException.From(res.StatusCode, "Fehler: 'message' ist null.", text);
|
|
throw ApiException.From(res.StatusCode, message, text);
|
|
} catch (ApiException) {
|
|
throw;
|
|
} catch (Exception) {
|
|
// Fallback: Wenn Parsing fehlschlägt oder "message" fehlt, konsistente Fehlermeldung wie BaseFunc
|
|
throw ApiException.From(res.StatusCode, "Fehler: 'message' ist null.", text);
|
|
}
|
|
}
|
|
|
|
if (typeof(T) == typeof(void) || typeof(T) == typeof(object) || string.IsNullOrWhiteSpace(text))
|
|
return default!;
|
|
|
|
var obj = System.Text.Json.JsonSerializer.Deserialize<T>(text, _json);
|
|
if (obj is null)
|
|
throw new ApiException("Fehler beim Deserialisieren der Daten.");
|
|
return obj;
|
|
}
|
|
|
|
public Task DeleteAsync(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default)
|
|
=> SendAsync<object>(HttpMethod.Delete, path, null, query, ct);
|
|
|
|
private void EnsureBaseAddress() {
|
|
var baseUrl = _settings.ApiUrl;
|
|
|
|
if (string.IsNullOrWhiteSpace(baseUrl)) {
|
|
throw new InvalidOperationException(
|
|
"ApiUrl ist leer. Bitte zuerst eine gültige Server-URL setzen (Preferences key 'apiUrl'), " +
|
|
"z.B. im Login/Setup, bevor API-Aufrufe stattfinden."
|
|
);
|
|
}
|
|
|
|
// nur setzen, wenn nötig (damit spätere Änderungen nach Login greifen)
|
|
if (_http.BaseAddress is null || !Uri.Equals(_http.BaseAddress, new Uri(baseUrl, UriKind.Absolute))) {
|
|
_http.BaseAddress = new Uri(baseUrl, UriKind.Absolute);
|
|
_http.Timeout = _options.Timeout;
|
|
}
|
|
}
|
|
|
|
private static string TryGetMessage(string text) {
|
|
try {
|
|
using var doc = JsonDocument.Parse(text);
|
|
if (doc.RootElement.TryGetProperty("message", out var m) && m.ValueKind == JsonValueKind.String)
|
|
return m.GetString() ?? text;
|
|
} catch {
|
|
}
|
|
|
|
return text;
|
|
}
|
|
|
|
private static Uri BuildUri(string path, IDictionary<string, string?>? query) {
|
|
if (query is null || query.Count == 0)
|
|
return new Uri(path, UriKind.Relative);
|
|
|
|
var sb = new StringBuilder(path);
|
|
sb.Append(path.Contains('?') ? '&' : '?');
|
|
sb.Append(string.Join('&', query
|
|
.Where(kv => kv.Value is not null)
|
|
.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value!)}")));
|
|
return new Uri(sb.ToString(), UriKind.Relative);
|
|
}
|
|
} |