API - CRUD complet sur /api/livres : liste filtrable (format, statut) et cherchable (titre, auteur), consultation, creation, modification, changement de statut isole, suppression. - Aucune lecture ne filtre sur AjoutePar : la bibliotheque est commune au foyer. Le critere n'existe meme pas dans CritereLivres, pour qu'on ne puisse pas s'en servir par inadvertance. Deux tests verrouillent l'invariant, dont un par reflexion sur le type des criteres. - AjoutePar est renseigne par le serveur a la creation, jamais par le client, et n'est pas reecrit a l'edition (pas plus que DateAjout). - Identite lue dans les en-tetes SSOwat (YNH_USER, YNH_USER_EMAIL, YNH_USER_FULLNAME), avec repli sur un utilisateur simule configure en developpement uniquement. Sans en-tete ni simulation, l'identite reste inconnue plutot qu'inventee. Expose par GET /api/moi. - Migration appliquee au demarrage : pas d'etape manuelle a l'installation. Interface Blazor, pensee mobile d'abord - Catalogue unique (physiques et numeriques melanges, distingues par le format) avec recherche, filtres et changement de statut directement dans la liste. - Ajout par ISBN : quand la cascade rend plusieurs notices, un ecran de choix les presente en mettant en avant editeur et annee, seuls elements qui les departagent. Le formulaire reste ensuite entierement modifiable. - Ajout manuel, edition, suppression avec confirmation explicite. - Couvertures : l'URL OpenLibrary n'est jamais validee par un appel prealable (502 intermittents mesures en phase 2) ; un attribut onerror masque l'image cassee et laisse apparaitre un substitut. Decisions prises la ou CLAUDE.md etait muet - Tri alphabetique par titre. - Un ISBN saisi doit etre valide, mais reste facultatif. - Ecran de choix affiche seulement a partir de deux candidats. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
146 lines
5.1 KiB
C#
146 lines
5.1 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using MaBibli.Shared.Catalogue;
|
|
using MaBibli.Shared.Dtos;
|
|
using MaBibli.Shared.Entites;
|
|
|
|
namespace MaBibli.Client.Services;
|
|
|
|
/// <summary>
|
|
/// Accès à l'API depuis le client Blazor.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Aucun cache : la phase 3 suppose le réseau disponible. Le cache hors-ligne est la phase 6.
|
|
/// </remarks>
|
|
public sealed class ServiceLivresApi(HttpClient http)
|
|
{
|
|
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
|
|
|
|
public async Task<IReadOnlyList<LivreDto>> ListerAsync(CritereLivres criteres, CancellationToken ct = default)
|
|
{
|
|
var parametres = new List<string>();
|
|
|
|
if (!string.IsNullOrWhiteSpace(criteres.Recherche))
|
|
{
|
|
parametres.Add($"recherche={Uri.EscapeDataString(criteres.Recherche.Trim())}");
|
|
}
|
|
|
|
if (criteres.Format is { } format)
|
|
{
|
|
parametres.Add($"format={format}");
|
|
}
|
|
|
|
if (criteres.Statut is { } statut)
|
|
{
|
|
parametres.Add($"statut={statut}");
|
|
}
|
|
|
|
var url = "api/livres" + (parametres.Count > 0 ? "?" + string.Join('&', parametres) : string.Empty);
|
|
|
|
return await http.GetFromJsonAsync<List<LivreDto>>(url, Json, ct) ?? [];
|
|
}
|
|
|
|
public async Task<LivreDto?> ObtenirAsync(int id, CancellationToken ct = default)
|
|
{
|
|
var reponse = await http.GetAsync($"api/livres/{id}", ct);
|
|
return reponse.StatusCode == HttpStatusCode.NotFound
|
|
? null
|
|
: await LireAsync<LivreDto>(reponse, ct);
|
|
}
|
|
|
|
public async Task<ResultatApi<LivreDto>> CreerAsync(EnregistrementLivre saisie, CancellationToken ct = default)
|
|
{
|
|
var reponse = await http.PostAsJsonAsync("api/livres", saisie, Json, ct);
|
|
return await InterpreterAsync<LivreDto>(reponse, ct);
|
|
}
|
|
|
|
public async Task<ResultatApi<LivreDto>> ModifierAsync(int id, EnregistrementLivre saisie, CancellationToken ct = default)
|
|
{
|
|
var reponse = await http.PutAsJsonAsync($"api/livres/{id}", saisie, Json, ct);
|
|
return await InterpreterAsync<LivreDto>(reponse, ct);
|
|
}
|
|
|
|
public async Task<ResultatApi<LivreDto>> ChangerStatutAsync(int id, Statut statut, CancellationToken ct = default)
|
|
{
|
|
var reponse = await http.PatchAsJsonAsync(
|
|
$"api/livres/{id}/statut", new ChangementStatut { Statut = statut }, Json, ct);
|
|
return await InterpreterAsync<LivreDto>(reponse, ct);
|
|
}
|
|
|
|
public async Task<string?> SupprimerAsync(int id, CancellationToken ct = default)
|
|
{
|
|
var reponse = await http.DeleteAsync($"api/livres/{id}", ct);
|
|
return reponse.IsSuccessStatusCode ? null : await MessageErreurAsync(reponse, ct);
|
|
}
|
|
|
|
public async Task<ResultatLookupIsbn?> ChercherIsbnAsync(string isbn, CancellationToken ct = default)
|
|
{
|
|
var reponse = await http.GetAsync($"api/isbn/{Uri.EscapeDataString(isbn)}", ct);
|
|
return reponse.IsSuccessStatusCode
|
|
? await LireAsync<ResultatLookupIsbn>(reponse, ct)
|
|
: null;
|
|
}
|
|
|
|
public async Task<UtilisateurCourant> ObtenirUtilisateurAsync(CancellationToken ct = default)
|
|
{
|
|
try
|
|
{
|
|
return await http.GetFromJsonAsync<UtilisateurCourant>("api/moi", Json, ct)
|
|
?? UtilisateurCourant.Anonyme;
|
|
}
|
|
catch (HttpRequestException)
|
|
{
|
|
return UtilisateurCourant.Anonyme;
|
|
}
|
|
}
|
|
|
|
private static async Task<T?> LireAsync<T>(HttpResponseMessage reponse, CancellationToken ct)
|
|
{
|
|
reponse.EnsureSuccessStatusCode();
|
|
return await reponse.Content.ReadFromJsonAsync<T>(Json, ct);
|
|
}
|
|
|
|
private static async Task<ResultatApi<T>> InterpreterAsync<T>(HttpResponseMessage reponse, CancellationToken ct)
|
|
{
|
|
if (reponse.IsSuccessStatusCode)
|
|
{
|
|
var valeur = await reponse.Content.ReadFromJsonAsync<T>(Json, ct);
|
|
return new ResultatApi<T>(valeur, null);
|
|
}
|
|
|
|
return new ResultatApi<T>(default, await MessageErreurAsync(reponse, ct));
|
|
}
|
|
|
|
/// <summary>Récupère le message d'erreur de l'API pour l'afficher tel quel à l'utilisateur.</summary>
|
|
private static async Task<string> MessageErreurAsync(HttpResponseMessage reponse, CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
var corps = await reponse.Content.ReadFromJsonAsync<ErreurApi>(Json, ct);
|
|
if (!string.IsNullOrWhiteSpace(corps?.Erreur))
|
|
{
|
|
return corps.Erreur;
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Corps illisible ou vide : on retombe sur le code HTTP.
|
|
}
|
|
|
|
return reponse.StatusCode switch
|
|
{
|
|
HttpStatusCode.NotFound => "Ce livre n'existe plus.",
|
|
_ => $"L'enregistrement a échoué ({(int)reponse.StatusCode}).",
|
|
};
|
|
}
|
|
|
|
private sealed record ErreurApi(string? Erreur);
|
|
}
|
|
|
|
/// <summary>Résultat d'un appel d'écriture : la valeur, ou un message d'erreur affichable.</summary>
|
|
public readonly record struct ResultatApi<T>(T? Valeur, string? Erreur)
|
|
{
|
|
public bool EstOk => Erreur is null;
|
|
}
|