From 8c175de95308884cbfad60a4d30cb650134e78a1 Mon Sep 17 00:00:00 2001 From: OlgunR Date: Fri, 16 Jan 2026 14:10:56 +0100 Subject: [PATCH] Refactor API client for richer error handling Refactored CatalogApiClient methods to return ApiResult for create, update, and delete operations, enabling more detailed error reporting. Introduced ApiResult and ProblemDetailsDto types, and added logic to parse and display informative error messages. Updated Catalogs.razor to use the new pattern and show user-friendly error feedback. Added necessary using directives. --- DbFirst.BlazorWasm/Pages/Catalogs.razor | 12 +- .../Services/CatalogApiClient.cs | 106 ++++++++++++++++-- 2 files changed, 104 insertions(+), 14 deletions(-) diff --git a/DbFirst.BlazorWasm/Pages/Catalogs.razor b/DbFirst.BlazorWasm/Pages/Catalogs.razor index 1a23acf..d9acb35 100644 --- a/DbFirst.BlazorWasm/Pages/Catalogs.razor +++ b/DbFirst.BlazorWasm/Pages/Catalogs.razor @@ -151,9 +151,9 @@ else if (isEditing) { var updated = await Api.UpdateAsync(editingId, formModel); - if (!updated) + if (!updated.Success) { - errorMessage = "Aktualisierung fehlgeschlagen."; + errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen."; return; } @@ -162,9 +162,9 @@ else else { var created = await Api.CreateAsync(formModel); - if (created == null) + if (!created.Success || created.Value == null) { - errorMessage = "Anlegen fehlgeschlagen."; + errorMessage = created.Error ?? "Anlegen fehlgeschlagen."; return; } @@ -195,9 +195,9 @@ else try { var deleted = await Api.DeleteAsync(id); - if (!deleted) + if (!deleted.Success) { - errorMessage = "Löschen fehlgeschlagen."; + errorMessage = deleted.Error ?? "Löschen fehlgeschlagen."; return; } diff --git a/DbFirst.BlazorWasm/Services/CatalogApiClient.cs b/DbFirst.BlazorWasm/Services/CatalogApiClient.cs index 6e548c8..d9d797d 100644 --- a/DbFirst.BlazorWasm/Services/CatalogApiClient.cs +++ b/DbFirst.BlazorWasm/Services/CatalogApiClient.cs @@ -1,4 +1,6 @@ +using System.Net; using System.Net.Http.Json; +using System.Text.Json; using DbFirst.BlazorWasm.Models; namespace DbFirst.BlazorWasm.Services; @@ -24,26 +26,114 @@ public class CatalogApiClient return await _httpClient.GetFromJsonAsync($"{Endpoint}/{id}"); } - public async Task CreateAsync(CatalogWriteDto dto) + public async Task> CreateAsync(CatalogWriteDto dto) { var response = await _httpClient.PostAsJsonAsync(Endpoint, dto); - if (!response.IsSuccessStatusCode) + if (response.IsSuccessStatusCode) { - return null; + var payload = await response.Content.ReadFromJsonAsync(); + return ApiResult.Ok(payload); } - return await response.Content.ReadFromJsonAsync(); + var error = await ReadErrorAsync(response); + return ApiResult.Fail(error); } - public async Task UpdateAsync(int id, CatalogWriteDto dto) + public async Task> UpdateAsync(int id, CatalogWriteDto dto) { var response = await _httpClient.PutAsJsonAsync($"{Endpoint}/{id}", dto); - return response.IsSuccessStatusCode; + if (response.IsSuccessStatusCode) + { + return ApiResult.Ok(true); + } + + var error = await ReadErrorAsync(response); + return ApiResult.Fail(error); } - public async Task DeleteAsync(int id) + public async Task> DeleteAsync(int id) { var response = await _httpClient.DeleteAsync($"{Endpoint}/{id}"); - return response.IsSuccessStatusCode; + if (response.IsSuccessStatusCode) + { + return ApiResult.Ok(true); + } + + var error = await ReadErrorAsync(response); + return ApiResult.Fail(error); + } + + private static async Task ReadErrorAsync(HttpResponseMessage response) + { + string? problemTitle = null; + string? problemDetail = null; + + try + { + var problem = await response.Content.ReadFromJsonAsync(); + if (problem != null) + { + problemTitle = problem.Title; + problemDetail = problem.Detail ?? problem.Type; + } + } + catch + { + // ignore parse errors + } + + var status = response.StatusCode; + var reason = response.ReasonPhrase; + var body = await response.Content.ReadAsStringAsync(); + + string detail = problemDetail; + if (string.IsNullOrWhiteSpace(detail) && !string.IsNullOrWhiteSpace(body)) + { + detail = body; + } + + // Friendly overrides + if (status == HttpStatusCode.Conflict) + { + return "Datensatz existiert bereits. Bitte wählen Sie einen anderen Titel."; + } + if (status == HttpStatusCode.BadRequest && (detail?.Contains("CatTitle cannot be changed", StringComparison.OrdinalIgnoreCase) ?? false)) + { + return "Titel kann nicht geändert werden."; + } + + return status switch + { + HttpStatusCode.BadRequest => $"Eingabe ungültig{FormatSuffix(problemTitle, detail, reason)}", + HttpStatusCode.NotFound => $"Nicht gefunden{FormatSuffix(problemTitle, detail, reason)}", + HttpStatusCode.Conflict => $"Konflikt{FormatSuffix(problemTitle, detail, reason)}", + HttpStatusCode.Unauthorized => $"Nicht autorisiert{FormatSuffix(problemTitle, detail, reason)}", + HttpStatusCode.Forbidden => $"Nicht erlaubt{FormatSuffix(problemTitle, detail, reason)}", + HttpStatusCode.InternalServerError => $"Serverfehler{FormatSuffix(problemTitle, detail, reason)}", + _ => $"Fehler {(int)status} {reason ?? string.Empty}{FormatSuffix(problemTitle, detail, reason)}" + }; } + + private static string FormatSuffix(string? title, string? detail, string? reason) + { + var parts = new List(); + if (!string.IsNullOrWhiteSpace(title)) parts.Add(title); + if (!string.IsNullOrWhiteSpace(detail)) parts.Add(detail); + if (parts.Count == 0 && !string.IsNullOrWhiteSpace(reason)) parts.Add(reason); + if (parts.Count == 0) return string.Empty; + return ": " + string.Join(" | ", parts); + } +} + +public record ApiResult(bool Success, T? Value, string? Error) +{ + public static ApiResult Ok(T? value) => new(true, value, null); + public static ApiResult Fail(string? error) => new(false, default, error); +} + +internal sealed class ProblemDetailsDto +{ + public string? Type { get; set; } + public string? Title { get; set; } + public string? Detail { get; set; } }