Signaler les doublons à l'ajout d'un livre, sans jamais les refuser
Rien n'empêchait de rescanner un livre déjà catalogué. Plutôt qu'un index unique — qui interdirait le second exemplaire, cas parfaitement légitime — la création répond 409 avec les fiches semblables, et « confirmerDoublon » enregistre la même saisie. Deux critères, dont aucun n'est une clé : ISBN identique, ou clé d'œuvre et auteur communs. La clé d'œuvre étant un préfixe de TitreNormalise, SQL dégrossit sur la colonne indexée et l'égalité exacte se vérifie ensuite en mémoire. Vérifié en exécution : avertissement, retour au formulaire intact, et ajout confirmé créant bien un second exemplaire. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -127,8 +127,62 @@ public sealed class ServiceLivresApi(HttpClient http, CacheHorsLigne cache, Etat
|
||||
return instantane?.Donnees.FirstOrDefault(l => l.Id == id);
|
||||
}
|
||||
|
||||
public async Task<ResultatApi<LivreDto>> CreerAsync(EnregistrementLivre saisie, CancellationToken ct = default) =>
|
||||
await EcrireAsync<LivreDto>(() => http.PostAsJsonAsync("api/livres", saisie, Json, ct), ct);
|
||||
/// <summary>
|
||||
/// Ajoute un livre, en rapportant le cas échéant ce que le catalogue a déjà de semblable.
|
||||
/// </summary>
|
||||
/// <param name="confirmerDoublon">
|
||||
/// Vrai quand l'utilisateur a vu l'avertissement et veut ajouter le livre malgré tout —
|
||||
/// second exemplaire, autre édition, ou rapprochement à côté de la plaque.
|
||||
/// </param>
|
||||
public async Task<ResultatCreation> CreerAsync(
|
||||
EnregistrementLivre saisie, bool confirmerDoublon = false, CancellationToken ct = default)
|
||||
{
|
||||
var url = "api/livres" + (confirmerDoublon ? "?confirmerDoublon=true" : string.Empty);
|
||||
var (reponse, motif) = await EnvoyerAsync(() => http.PostAsJsonAsync(url, saisie, Json, ct));
|
||||
|
||||
if (reponse is null)
|
||||
{
|
||||
return new ResultatCreation(null, motif, null);
|
||||
}
|
||||
|
||||
// Le 409 n'est PAS une erreur : le serveur demande une confirmation, et l'écran doit
|
||||
// proposer d'ajouter quand même. Le faire passer par le message d'erreur générique le
|
||||
// transformerait en échec, avec le livre perdu au passage.
|
||||
if (reponse.StatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
var doublons = await LireDoublonsAsync(reponse, ct);
|
||||
if (doublons is not null)
|
||||
{
|
||||
return new ResultatCreation(null, null, doublons);
|
||||
}
|
||||
}
|
||||
|
||||
var resultat = await InterpreterAsync<LivreDto>(reponse, ct);
|
||||
|
||||
if (resultat.EstOk)
|
||||
{
|
||||
_ = RafraichirEnFondAsync();
|
||||
}
|
||||
|
||||
return new ResultatCreation(resultat.Valeur, resultat.Erreur, null);
|
||||
}
|
||||
|
||||
/// <summary>Lit le corps d'un 409, ou <c>null</c> s'il est illisible.</summary>
|
||||
private static async Task<DoublonsLivre?> LireDoublonsAsync(
|
||||
HttpResponseMessage reponse, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var doublons = await reponse.Content.ReadFromJsonAsync<DoublonsLivre>(Json, ct);
|
||||
return string.IsNullOrWhiteSpace(doublons?.Message) ? null : doublons;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Corps inattendu : on retombe sur le traitement d'erreur ordinaire, qui dira au
|
||||
// moins que l'enregistrement n'a pas eu lieu.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ResultatApi<LivreDto>> ModifierAsync(int id, EnregistrementLivre saisie, CancellationToken ct = default) =>
|
||||
await EcrireAsync<LivreDto>(() => http.PutAsJsonAsync($"api/livres/{id}", saisie, Json, ct), ct);
|
||||
@@ -399,31 +453,52 @@ public sealed class ServiceLivresApi(HttpClient http, CacheHorsLigne cache, Etat
|
||||
/// </remarks>
|
||||
private async Task<ResultatApi<T>> EcrireAsync<T>(
|
||||
Func<Task<HttpResponseMessage>> appel, CancellationToken ct)
|
||||
{
|
||||
var (reponse, motif) = await EnvoyerAsync(appel);
|
||||
|
||||
if (reponse is null)
|
||||
{
|
||||
return new ResultatApi<T>(default, motif);
|
||||
}
|
||||
|
||||
var resultat = await InterpreterAsync<T>(reponse, ct);
|
||||
|
||||
if (resultat.EstOk)
|
||||
{
|
||||
// La base a changé : l'instantané doit suivre, sinon une coupure juste après une
|
||||
// modification montrerait un catalogue d'avant.
|
||||
_ = RafraichirEnFondAsync();
|
||||
}
|
||||
|
||||
return resultat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Envoie une écriture : soit une réponse à interpréter, soit le motif de son impossibilité.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Séparé de <see cref="EcrireAsync{T}"/> parce que toutes les écritures ne s'interprètent
|
||||
/// pas de la même façon : la création doit reconnaître le 409 des doublons, qui n'est pas un
|
||||
/// échec. Le refus hors-ligne et le signalement de l'état du réseau, eux, sont communs.
|
||||
/// </remarks>
|
||||
private async Task<(HttpResponseMessage? Reponse, string? Motif)> EnvoyerAsync(
|
||||
Func<Task<HttpResponseMessage>> appel)
|
||||
{
|
||||
if (!reseau.EnLigne)
|
||||
{
|
||||
return new ResultatApi<T>(default, EtatReseau.MotifHorsLigne);
|
||||
return (null, EtatReseau.MotifHorsLigne);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var reponse = await appel();
|
||||
reseau.SignalerSuccesReseau();
|
||||
var resultat = await InterpreterAsync<T>(reponse, ct);
|
||||
|
||||
if (resultat.EstOk)
|
||||
{
|
||||
// La base a changé : l'instantané doit suivre, sinon une coupure juste après une
|
||||
// modification montrerait un catalogue d'avant.
|
||||
_ = RafraichirEnFondAsync();
|
||||
}
|
||||
|
||||
return resultat;
|
||||
return (reponse, null);
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
reseau.SignalerEchecReseau();
|
||||
return new ResultatApi<T>(default, EtatReseau.MotifHorsLigne);
|
||||
return (null, EtatReseau.MotifHorsLigne);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,3 +740,16 @@ public readonly record struct ResultatApi<T>(T? Valeur, string? Erreur)
|
||||
{
|
||||
public bool EstOk => Erreur is null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Résultat d'un ajout au catalogue : le livre, une erreur, ou des doublons à confirmer.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Trois issues et non deux : un doublon signalé n'est <b>pas</b> une erreur. La même saisie,
|
||||
/// confirmée, s'enregistrera telle quelle — l'écran doit donc offrir « Ajouter quand même »
|
||||
/// plutôt qu'un message rouge qui laisserait croire à un refus.
|
||||
/// </remarks>
|
||||
public readonly record struct ResultatCreation(LivreDto? Livre, string? Erreur, DoublonsLivre? Doublons)
|
||||
{
|
||||
public bool EstOk => Livre is not null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user