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; } +}