Refactor API client for richer error handling

Refactored CatalogApiClient methods to return ApiResult<T> for create, update, and delete operations, enabling more detailed error reporting. Introduced ApiResult<T> 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.
This commit is contained in:
OlgunR
2026-01-16 14:10:56 +01:00
parent 1fd776bc29
commit 8c175de953
2 changed files with 104 additions and 14 deletions

View File

@@ -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<CatalogReadDto>($"{Endpoint}/{id}");
}
public async Task<CatalogReadDto?> CreateAsync(CatalogWriteDto dto)
public async Task<ApiResult<CatalogReadDto?>> CreateAsync(CatalogWriteDto dto)
{
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto);
if (!response.IsSuccessStatusCode)
if (response.IsSuccessStatusCode)
{
return null;
var payload = await response.Content.ReadFromJsonAsync<CatalogReadDto>();
return ApiResult<CatalogReadDto?>.Ok(payload);
}
return await response.Content.ReadFromJsonAsync<CatalogReadDto>();
var error = await ReadErrorAsync(response);
return ApiResult<CatalogReadDto?>.Fail(error);
}
public async Task<bool> UpdateAsync(int id, CatalogWriteDto dto)
public async Task<ApiResult<bool>> UpdateAsync(int id, CatalogWriteDto dto)
{
var response = await _httpClient.PutAsJsonAsync($"{Endpoint}/{id}", dto);
return response.IsSuccessStatusCode;
if (response.IsSuccessStatusCode)
{
return ApiResult<bool>.Ok(true);
}
var error = await ReadErrorAsync(response);
return ApiResult<bool>.Fail(error);
}
public async Task<bool> DeleteAsync(int id)
public async Task<ApiResult<bool>> DeleteAsync(int id)
{
var response = await _httpClient.DeleteAsync($"{Endpoint}/{id}");
return response.IsSuccessStatusCode;
if (response.IsSuccessStatusCode)
{
return ApiResult<bool>.Ok(true);
}
var error = await ReadErrorAsync(response);
return ApiResult<bool>.Fail(error);
}
private static async Task<string> ReadErrorAsync(HttpResponseMessage response)
{
string? problemTitle = null;
string? problemDetail = null;
try
{
var problem = await response.Content.ReadFromJsonAsync<ProblemDetailsDto>();
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<string>();
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<T>(bool Success, T? Value, string? Error)
{
public static ApiResult<T> Ok(T? value) => new(true, value, null);
public static ApiResult<T> 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; }
}