Merge branch 'dev' into j3/feat/pagination
This commit is contained in:
@@ -74,7 +74,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
chmod +x scripts/test-endpoints.sh
|
chmod +x scripts/test-endpoints.sh
|
||||||
bash scripts/test-endpoints.sh http://localhost:5038 1000 2>&1 | tee /tmp/webzine_endpoint_output.txt
|
bash scripts/test-endpoints.sh http://localhost:5038 1000 2>&1 | tee /tmp/webzine_endpoint_output.txt
|
||||||
EXIT_CODE=${PIPESTATUS[0]}
|
|
||||||
|
|
||||||
FAIL_COUNT=$(grep -cE "^\[ÉCHEC\]" /tmp/webzine_endpoint_output.txt 2>/dev/null || echo 0)
|
FAIL_COUNT=$(grep -cE "^\[ÉCHEC\]" /tmp/webzine_endpoint_output.txt 2>/dev/null || echo 0)
|
||||||
SLOW_COUNT=$(grep -cE "^\[LENT\]" /tmp/webzine_endpoint_output.txt 2>/dev/null || echo 0)
|
SLOW_COUNT=$(grep -cE "^\[LENT\]" /tmp/webzine_endpoint_output.txt 2>/dev/null || echo 0)
|
||||||
@@ -137,10 +136,4 @@ jobs:
|
|||||||
-H "Authorization: token $GITEA_TOKEN" \
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "$(jq -n --arg body "$BODY" '{body: $body}')" \
|
-d "$(jq -n --arg body "$BODY" '{body: $body}')" \
|
||||||
"$GITEA_SERVER_URL/api/v1/repos/$REPO/issues/$PR_NUMBER/comments"
|
"$GITEA_SERVER_URL/api/v1/repos/$REPO/issues/$PR_NUMBER/comments"
|
||||||
|
|
||||||
- name: Fail job if performance issues detected
|
|
||||||
if: steps.perf_test.outputs.failed > 0 || steps.perf_test.outputs.slow > 0
|
|
||||||
run: |
|
|
||||||
echo "❌ Job failed due to performance issues"
|
|
||||||
exit 1
|
|
||||||
57
Webzine.Business.Contracts/Dto/TitreAdminDTO.cs
Normal file
57
Webzine.Business.Contracts/Dto/TitreAdminDTO.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
namespace Webzine.Business.Contracts.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dto transportant les données métier d'un titre saisi en administration.
|
||||||
|
/// </summary>
|
||||||
|
public class TitreAdminDTO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Identifiant du titre (0 lors d'une création).
|
||||||
|
/// </summary>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identifiant de l'artiste sélectionné.
|
||||||
|
/// </summary>
|
||||||
|
public int IdArtiste { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Libellé du titre.
|
||||||
|
/// </summary>
|
||||||
|
public string Libelle { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nom de l'album.
|
||||||
|
/// </summary>
|
||||||
|
public string Album { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Texte de la chronique.
|
||||||
|
/// </summary>
|
||||||
|
public string Chronique { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Date de sortie du titre.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime DateSortie { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Durée en secondes.
|
||||||
|
/// </summary>
|
||||||
|
public int Duree { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL de la jaquette.
|
||||||
|
/// </summary>
|
||||||
|
public string UrlJaquette { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL d'écoute.
|
||||||
|
/// </summary>
|
||||||
|
public string? UrlEcoute { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identifiants des styles sélectionnés.
|
||||||
|
/// </summary>
|
||||||
|
public List<int> Styles { get; set; } = new ();
|
||||||
|
}
|
||||||
22
Webzine.Business.Contracts/ITitreAdminService.cs
Normal file
22
Webzine.Business.Contracts/ITitreAdminService.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
namespace Webzine.Business.Contracts;
|
||||||
|
|
||||||
|
using Webzine.Business.Contracts.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service responsable des opérations d'administration sur les titres.
|
||||||
|
/// Orchestre la résolution des dépendances (artiste, styles) et la persistance.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITitreAdminService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Crée un nouveau titre à partir des données du formulaire d'administration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="commande">Les données saisies dans le formulaire de création.</param>
|
||||||
|
void CreerTitre(TitreAdminDTO commande);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Met à jour un titre existant à partir des données du formulaire d'administration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="commande">Les données saisies dans le formulaire de modification.</param>
|
||||||
|
void ModifierTitre(TitreAdminDTO commande);
|
||||||
|
}
|
||||||
122
Webzine.Business/TitreAdminService.cs
Normal file
122
Webzine.Business/TitreAdminService.cs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
namespace Webzine.Business;
|
||||||
|
|
||||||
|
using Webzine.Business.Contracts;
|
||||||
|
using Webzine.Business.Contracts.Dto;
|
||||||
|
using Webzine.Entity;
|
||||||
|
using Webzine.Repository.Contracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implémentation de <see cref="ITitreAdminService"/>.
|
||||||
|
/// Orchestre la résolution des styles, la construction de l'entité
|
||||||
|
/// et la délégation au repository.
|
||||||
|
/// </summary>
|
||||||
|
public class TitreAdminService : ITitreAdminService
|
||||||
|
{
|
||||||
|
private readonly ITitreRepository titreRepository;
|
||||||
|
private readonly IArtisteRepository artisteRepository;
|
||||||
|
private readonly IStyleRepository styleRepository;
|
||||||
|
private readonly ILogger<TitreAdminService> logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="TitreAdminService"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="titreRepository">Repository des titres.</param>
|
||||||
|
/// <param name="artisteRepository">Repository des artistes.</param>
|
||||||
|
/// <param name="styleRepository">Repository des styles.</param>
|
||||||
|
/// <param name="logger">Service de journalisation.</param>
|
||||||
|
public TitreAdminService(
|
||||||
|
ITitreRepository titreRepository,
|
||||||
|
IArtisteRepository artisteRepository,
|
||||||
|
IStyleRepository styleRepository,
|
||||||
|
ILogger<TitreAdminService> logger)
|
||||||
|
{
|
||||||
|
this.titreRepository = titreRepository;
|
||||||
|
this.artisteRepository = artisteRepository;
|
||||||
|
this.styleRepository = styleRepository;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void CreerTitre(TitreAdminDTO commande)
|
||||||
|
{
|
||||||
|
this.logger.LogInformation(
|
||||||
|
"Création d'un nouveau titre '{Libelle}' pour l'artiste ID {IdArtiste}.",
|
||||||
|
commande.Libelle,
|
||||||
|
commande.IdArtiste);
|
||||||
|
|
||||||
|
Artiste artiste = this.artisteRepository.Find(commande.IdArtiste);
|
||||||
|
List<Style> styles = this.ResoudreStyles(commande.Styles);
|
||||||
|
|
||||||
|
var titre = new Titre
|
||||||
|
{
|
||||||
|
IdArtiste = artiste.IdArtiste,
|
||||||
|
Artiste = artiste,
|
||||||
|
Libelle = commande.Libelle,
|
||||||
|
Album = commande.Album,
|
||||||
|
Chronique = commande.Chronique,
|
||||||
|
DateCreation = DateTime.UtcNow,
|
||||||
|
DateSortie = commande.DateSortie,
|
||||||
|
Duree = commande.Duree,
|
||||||
|
UrlJaquette = commande.UrlJaquette,
|
||||||
|
UrlEcoute = commande.UrlEcoute ?? string.Empty,
|
||||||
|
Styles = styles,
|
||||||
|
Commentaires = new List<Commentaire>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.titreRepository.Add(titre);
|
||||||
|
|
||||||
|
this.logger.LogInformation("Titre '{Libelle}' créé avec succès (ID {IdTitre}).", titre.Libelle, titre.IdTitre);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void ModifierTitre(TitreAdminDTO commande)
|
||||||
|
{
|
||||||
|
this.logger.LogInformation("Modification du titre ID {Id} ('{Libelle}').", commande.Id, commande.Libelle);
|
||||||
|
|
||||||
|
List<Style> styles = this.ResoudreStyles(commande.Styles);
|
||||||
|
|
||||||
|
// On charge le titre existant pour ne pas écraser NbLectures / NbLikes
|
||||||
|
Titre existant = this.titreRepository.Find(commande.Id);
|
||||||
|
|
||||||
|
existant.IdArtiste = commande.IdArtiste;
|
||||||
|
existant.Libelle = commande.Libelle;
|
||||||
|
existant.Album = commande.Album;
|
||||||
|
existant.Chronique = commande.Chronique;
|
||||||
|
existant.DateSortie = commande.DateSortie;
|
||||||
|
existant.Duree = commande.Duree;
|
||||||
|
existant.UrlJaquette = commande.UrlJaquette;
|
||||||
|
existant.UrlEcoute = commande.UrlEcoute ?? string.Empty;
|
||||||
|
|
||||||
|
existant.Styles.Clear();
|
||||||
|
foreach (var style in styles)
|
||||||
|
{
|
||||||
|
existant.Styles.Add(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.titreRepository.Update(existant);
|
||||||
|
|
||||||
|
this.logger.LogInformation("Titre ID {Id} modifié avec succès.", commande.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Résout les entités <see cref="Style"/> à partir d'une liste d'identifiants.
|
||||||
|
/// Les identifiants introuvables sont ignorés avec un avertissement.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="styleIds">Identifiants des styles sélectionnés.</param>
|
||||||
|
/// <returns>Liste des styles résolus.</returns>
|
||||||
|
private List<Style> ResoudreStyles(List<int> styleIds)
|
||||||
|
{
|
||||||
|
var styles = new List<Style>();
|
||||||
|
|
||||||
|
foreach (int id in styleIds)
|
||||||
|
{
|
||||||
|
Style style = this.styleRepository.Find(id);
|
||||||
|
|
||||||
|
styles.Add(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.LogDebug("{NbResolus}/{NbDemandes} styles résolus avec succès.", styles.Count, styleIds.Count);
|
||||||
|
|
||||||
|
return styles;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,14 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Webzine.Business.Contracts\Webzine.Business.Contracts.csproj" />
|
<ProjectReference Include="..\Webzine.Business.Contracts\Webzine.Business.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\Webzine.Entity\Webzine.Entity.csproj" />
|
||||||
<ProjectReference Include="..\Webzine.Repository.Contracts\Webzine.Repository.Contracts.csproj" />
|
<ProjectReference Include="..\Webzine.Repository.Contracts\Webzine.Repository.Contracts.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.Extensions.Logging.Abstractions">
|
||||||
|
<HintPath>..\..\..\..\..\..\..\.nuget\packages\microsoft.extensions.logging.abstractions\10.0.5\lib\net10.0\Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -39,6 +39,6 @@ namespace Webzine.Repository.Contracts
|
|||||||
/// récupérer les commentaires.</param>
|
/// récupérer les commentaires.</param>
|
||||||
/// <param name="limit">Le nombre maximum de commentaires à récupérer.</param>
|
/// <param name="limit">Le nombre maximum de commentaires à récupérer.</param>
|
||||||
/// <returns>Une collection de commentaires paginée.</returns>
|
/// <returns>Une collection de commentaires paginée.</returns>
|
||||||
IEnumerable<Commentaire> Paginate(int offset, int limit);
|
IEnumerable<Commentaire> FindCommentaires(int offset, int limit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ namespace Webzine.Repository
|
|||||||
{
|
{
|
||||||
Artiste artiste = this.context.Artistes
|
Artiste artiste = this.context.Artistes
|
||||||
.Include(a => a.Titres)
|
.Include(a => a.Titres)
|
||||||
.FirstOrDefault(a => a.IdArtiste == id);
|
.SingleOrDefault(a => a.IdArtiste == id);
|
||||||
return artiste;
|
return artiste;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -112,11 +112,12 @@ namespace Webzine.Repository
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// .AsNoTracking() rend la requête beaucoup plus rapide pour de la lecture
|
// .AsNoTracking() rend la requête beaucoup plus rapide pour de la lecture
|
||||||
|
// Pas besoin de faire un ToList() ici, car on retourne un IEnumerable<Artiste> et EF Core gère l'exécution différée de la requête.
|
||||||
var artistes = this.context.Artistes
|
var artistes = this.context.Artistes
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(t => t.Titres)
|
.Include(t => t.Titres);
|
||||||
.ToList();
|
|
||||||
this.logger.LogDebug("{Count} artistes récupérés de la base.", artistes.Count);
|
this.logger.LogDebug("La liste d'artistes a été récupérée de la base.");
|
||||||
return artistes;
|
return artistes;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -131,6 +132,13 @@ namespace Webzine.Repository
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
Artiste existingArtiste = this.Find(artiste.IdArtiste); // Vérifie que l'artiste existe avant de tenter de le mettre à jour
|
||||||
|
if (existingArtiste == null)
|
||||||
|
{
|
||||||
|
this.logger.LogWarning("L'artiste {Id} n'a pas été trouvé pour l'update.", artiste.IdArtiste);
|
||||||
|
throw new InvalidOperationException($"L'artiste avec l'ID {artiste.IdArtiste} n'a pas été trouvé pour la mise à jour.");
|
||||||
|
}
|
||||||
|
|
||||||
this.context.Artistes.Update(artiste);
|
this.context.Artistes.Update(artiste);
|
||||||
this.context.SaveChanges();
|
this.context.SaveChanges();
|
||||||
this.logger.LogDebug("Artiste {Id} ({Nom}) mis à jour avec succès.", artiste.IdArtiste, artiste.Nom);
|
this.logger.LogDebug("Artiste {Id} ({Nom}) mis à jour avec succès.", artiste.IdArtiste, artiste.Nom);
|
||||||
@@ -159,8 +167,7 @@ namespace Webzine.Repository
|
|||||||
var artiste = this.context.Artistes
|
var artiste = this.context.Artistes
|
||||||
.Where(a => a.Nom.ToLower().Contains(mot.ToLower()))
|
.Where(a => a.Nom.ToLower().Contains(mot.ToLower()))
|
||||||
.Include(t => t.Titres)
|
.Include(t => t.Titres)
|
||||||
.AsNoTracking()
|
.AsNoTracking();
|
||||||
.ToList();
|
|
||||||
return artiste;
|
return artiste;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ public class DbCommentaireRepository : ICommentaireRepository
|
|||||||
// On inclut le titre car il est souvent affiché avec le commentaire
|
// On inclut le titre car il est souvent affiché avec le commentaire
|
||||||
return this.context.Commentaires
|
return this.context.Commentaires
|
||||||
.Include(c => c.Titre)
|
.Include(c => c.Titre)
|
||||||
.FirstOrDefault(c => c.IdCommentaire == idCommentaire);
|
.SingleOrDefault(c => c.IdCommentaire == idCommentaire);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -86,27 +86,32 @@ public class DbCommentaireRepository : ICommentaireRepository
|
|||||||
var commentaires = this.context.Commentaires
|
var commentaires = this.context.Commentaires
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(c => c.Titre)
|
.Include(c => c.Titre)
|
||||||
.OrderByDescending(c => c.DateCreation)
|
.OrderByDescending(c => c.DateCreation);
|
||||||
.ToList();
|
|
||||||
|
|
||||||
this.logger.LogDebug("Nombre de commentaires trouvés : {Count}", commentaires.Count);
|
this.logger.LogDebug("La liste de commentaires a été récupérée.");
|
||||||
return commentaires;
|
return commentaires;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IEnumerable<Commentaire> Paginate(int offset, int limit)
|
public IEnumerable<Commentaire> FindCommentaires(int offset, int limit)
|
||||||
{
|
{
|
||||||
this.logger.LogDebug("Recherche paginée des commentaires (offset : {Offset}, limit : {Limit})", offset, limit);
|
try
|
||||||
|
{
|
||||||
|
this.logger.LogDebug("Recherche paginée des commentaires (offset : {Offset}, limit : {Limit})", offset, limit);
|
||||||
|
|
||||||
var commentaires = this.context.Commentaires
|
var commentaires = this.context.Commentaires
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(c => c.Titre)
|
.Include(c => c.Titre)
|
||||||
.OrderByDescending(c => c.DateCreation)
|
.OrderByDescending(c => c.DateCreation)
|
||||||
.Skip(offset)
|
.Skip(offset)
|
||||||
.Take(limit)
|
.Take(limit);
|
||||||
.ToList();
|
|
||||||
|
|
||||||
this.logger.LogDebug("{Count} commentaires trouvés pour cette page", commentaires.Count);
|
return commentaires;
|
||||||
return commentaires;
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
this.logger.LogError(ex, "Erreur lors de la pagination des commentaires (offset : {Offset}, limit : {Limit})", offset, limit);
|
||||||
|
throw new Exception("Une erreur est survenue lors de la pagination des commentaires.", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,7 +96,7 @@ public class DbStyleRepository : IStyleRepository
|
|||||||
var style = this.context.Styles
|
var style = this.context.Styles
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(s => s.Titres)
|
.Include(s => s.Titres)
|
||||||
.FirstOrDefault(s => s.IdStyle == id);
|
.SingleOrDefault(s => s.IdStyle == id);
|
||||||
|
|
||||||
if (style == null)
|
if (style == null)
|
||||||
{
|
{
|
||||||
@@ -124,10 +124,9 @@ public class DbStyleRepository : IStyleRepository
|
|||||||
|
|
||||||
var styles = this.context.Styles
|
var styles = this.context.Styles
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.OrderBy(s => s.Libelle)
|
.OrderBy(s => s.Libelle);
|
||||||
.ToList();
|
|
||||||
|
|
||||||
this.logger.LogDebug("{Count} styles récupérés", styles.Count);
|
this.logger.LogDebug("La liste de styles a été récupérée.");
|
||||||
return styles;
|
return styles;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -143,15 +142,15 @@ public class DbStyleRepository : IStyleRepository
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.logger.LogInformation("Mise à jour du style avec l'ID: {IdStyle}", style.IdStyle);
|
this.logger.LogInformation("Mise à jour du style avec l'ID: {IdStyle}", style.IdStyle);
|
||||||
|
Style existingStyle = this.Find(style.IdStyle); // Vérifie que le style existe avant de tenter de le mettre à jour
|
||||||
|
|
||||||
var existingStyle = this.context.Styles.Find(style.IdStyle);
|
|
||||||
if (existingStyle == null)
|
if (existingStyle == null)
|
||||||
{
|
{
|
||||||
this.logger.LogWarning("Style avec l'ID {IdStyle} non trouvé pour la mise à jour", style.IdStyle);
|
this.logger.LogWarning("Style avec l'ID {IdStyle} non trouvé pour l'update.", style.IdStyle);
|
||||||
throw new InvalidOperationException($"Style avec l'ID {style.IdStyle} non trouvé.");
|
throw new InvalidOperationException($"Style avec l'ID {style.IdStyle} non trouvé pour la mise à jour.");
|
||||||
}
|
}
|
||||||
|
|
||||||
existingStyle.Libelle = style.Libelle;
|
this.context.Styles.Update(style);
|
||||||
|
|
||||||
this.context.SaveChanges();
|
this.context.SaveChanges();
|
||||||
this.logger.LogDebug("Style mis à jour avec succès: {IdStyle}", style.IdStyle);
|
this.logger.LogDebug("Style mis à jour avec succès: {IdStyle}", style.IdStyle);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ public class DbTitreRepository : ITitreRepository
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.logger.LogInformation("Ajout d'un nouveau titre: {Libelle}", titre.Libelle);
|
this.logger.LogInformation("Ajout d'un nouveau titre: {Libelle}", titre.Libelle);
|
||||||
|
this.logger.LogDebug("Début de l'ajout du titre en base de données");
|
||||||
|
|
||||||
this.context.Titres.Add(titre);
|
this.context.Titres.Add(titre);
|
||||||
this.context.SaveChanges();
|
this.context.SaveChanges();
|
||||||
@@ -56,7 +57,8 @@ public class DbTitreRepository : ITitreRepository
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var count = Enumerable.Count(this.context.Titres);
|
this.logger.LogDebug("Comptage des titres en base de données");
|
||||||
|
var count = this.context.Titres.Count();
|
||||||
this.logger.LogDebug("Nombre total de titres: {Count}", count);
|
this.logger.LogDebug("Nombre total de titres: {Count}", count);
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
@@ -73,6 +75,7 @@ public class DbTitreRepository : ITitreRepository
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.logger.LogInformation("Suppression du titre avec l'ID: {IdTitre}", titre.IdTitre);
|
this.logger.LogInformation("Suppression du titre avec l'ID: {IdTitre}", titre.IdTitre);
|
||||||
|
this.logger.LogDebug("Début de la suppression du titre en base de données");
|
||||||
|
|
||||||
this.context.Titres.Remove(titre);
|
this.context.Titres.Remove(titre);
|
||||||
this.context.SaveChanges();
|
this.context.SaveChanges();
|
||||||
@@ -97,6 +100,7 @@ public class DbTitreRepository : ITitreRepository
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.logger.LogDebug("Recherche des titres avec offset: {Offset}, limit: {Limit}", offset, limit);
|
this.logger.LogDebug("Recherche des titres avec offset: {Offset}, limit: {Limit}", offset, limit);
|
||||||
|
this.logger.LogDebug("Préparation de la requête avec les inclusions Artiste et Styles");
|
||||||
|
|
||||||
var titres = this.context.Titres
|
var titres = this.context.Titres
|
||||||
.OrderByDescending(t => t.DateCreation)
|
.OrderByDescending(t => t.DateCreation)
|
||||||
@@ -122,10 +126,12 @@ public class DbTitreRepository : ITitreRepository
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.logger.LogInformation("Incrémentation du nombre de lectures pour le titre ID: {IdTitre}", titre.IdTitre);
|
this.logger.LogInformation("Incrémentation du nombre de lectures pour le titre ID: {IdTitre}", titre.IdTitre);
|
||||||
|
this.logger.LogDebug("Recherche du titre en base de données");
|
||||||
|
|
||||||
var existingTitre = this.context.Titres.Find(titre.IdTitre);
|
var existingTitre = this.context.Titres.Find(titre.IdTitre);
|
||||||
if (existingTitre != null)
|
if (existingTitre != null)
|
||||||
{
|
{
|
||||||
|
this.logger.LogDebug("Titre trouvé, incrémentation du compteur de lectures");
|
||||||
existingTitre.NbLectures++;
|
existingTitre.NbLectures++;
|
||||||
this.context.SaveChanges();
|
this.context.SaveChanges();
|
||||||
this.logger.LogDebug("Nouveau nombre de lectures: {NbLectures}", existingTitre.NbLectures);
|
this.logger.LogDebug("Nouveau nombre de lectures: {NbLectures}", existingTitre.NbLectures);
|
||||||
@@ -153,14 +159,20 @@ public class DbTitreRepository : ITitreRepository
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.logger.LogInformation("Incrémentation du nombre de likes pour le titre ID: {IdTitre}", titre.IdTitre);
|
this.logger.LogInformation("Incrémentation du nombre de likes pour le titre ID: {IdTitre}", titre.IdTitre);
|
||||||
|
this.logger.LogDebug("Recherche du titre en base de données");
|
||||||
|
|
||||||
var existingTitre = this.context.Titres.Find(titre.IdTitre);
|
var existingTitre = this.context.Titres.Find(titre.IdTitre);
|
||||||
if (existingTitre != null)
|
if (existingTitre != null)
|
||||||
{
|
{
|
||||||
|
this.logger.LogDebug("Titre trouvé, incrémentation du compteur de likes");
|
||||||
existingTitre.NbLikes++;
|
existingTitre.NbLikes++;
|
||||||
this.context.SaveChanges();
|
this.context.SaveChanges();
|
||||||
this.logger.LogDebug("Nouveau nombre de likes: {NbLikes}", existingTitre.NbLikes);
|
this.logger.LogDebug("Nouveau nombre de likes: {NbLikes}", existingTitre.NbLikes);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.logger.LogWarning("Titre avec l'ID {IdTitre} non trouvé pour l'incrémentation des likes", titre.IdTitre);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (DbUpdateException ex)
|
catch (DbUpdateException ex)
|
||||||
{
|
{
|
||||||
@@ -181,7 +193,7 @@ public class DbTitreRepository : ITitreRepository
|
|||||||
{
|
{
|
||||||
this.logger.LogInformation("Mise à jour du titre avec l'ID: {IdTitre}", titre.IdTitre);
|
this.logger.LogInformation("Mise à jour du titre avec l'ID: {IdTitre}", titre.IdTitre);
|
||||||
|
|
||||||
var existingTitre = this.context.Titres.Find(titre.IdTitre);
|
Titre existingTitre = this.Find(titre.IdTitre);
|
||||||
if (existingTitre != null)
|
if (existingTitre != null)
|
||||||
{
|
{
|
||||||
this.context.Entry(existingTitre).CurrentValues.SetValues(titre);
|
this.context.Entry(existingTitre).CurrentValues.SetValues(titre);
|
||||||
@@ -221,6 +233,7 @@ public class DbTitreRepository : ITitreRepository
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.logger.LogInformation("Recherche des titres avec le mot-clé: {Mot}", mot);
|
this.logger.LogInformation("Recherche des titres avec le mot-clé: {Mot}", mot);
|
||||||
|
this.logger.LogDebug("Préparation de la requête de recherche avec les inclusions");
|
||||||
|
|
||||||
var titres = this.context.Titres
|
var titres = this.context.Titres
|
||||||
.Include(t => t.Artiste)
|
.Include(t => t.Artiste)
|
||||||
@@ -272,12 +285,14 @@ public class DbTitreRepository : ITitreRepository
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
this.logger.LogDebug("Récupération de tous les titres");
|
||||||
|
this.logger.LogDebug("Préparation de la requête avec les inclusions Artiste et Styles");
|
||||||
|
|
||||||
var titres = this.context.Titres
|
var titres = this.context.Titres
|
||||||
.Include(t => t.Artiste)
|
.Include(t => t.Artiste)
|
||||||
.Include(t => t.Styles)
|
.Include(t => t.Styles)
|
||||||
.Include(t => t.Commentaires)
|
.Include(t => t.Commentaires)
|
||||||
.OrderBy(t => t.Libelle)
|
.OrderBy(t => t.Libelle)
|
||||||
.AsNoTracking()
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
this.logger.LogDebug("{Count} titres récupérés", titres.Count);
|
this.logger.LogDebug("{Count} titres récupérés", titres.Count);
|
||||||
@@ -296,13 +311,13 @@ public class DbTitreRepository : ITitreRepository
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.logger.LogInformation("Recherche des titres par style: {Libelle}", libelle);
|
this.logger.LogInformation("Recherche des titres par style: {Libelle}", libelle);
|
||||||
|
this.logger.LogDebug("Préparation de la requête de recherche par style");
|
||||||
|
|
||||||
var titres = this.context.Titres
|
var titres = this.context.Titres
|
||||||
.Include(t => t.Artiste)
|
.Include(t => t.Artiste)
|
||||||
.Include(t => t.Styles)
|
.Include(t => t.Styles)
|
||||||
.Where(t => t.Styles.Any(s => s.Libelle.ToLower() == libelle.ToLower()))
|
.Where(t => t.Styles.Any(s => s.Libelle.ToLower() == libelle.ToLower()))
|
||||||
.OrderBy(t => t.Libelle)
|
.OrderBy(t => t.Libelle)
|
||||||
.AsNoTracking()
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
this.logger.LogDebug("{Count} titres trouvés pour le style '{Libelle}'", titres.Count, libelle);
|
this.logger.LogDebug("{Count} titres trouvés pour le style '{Libelle}'", titres.Count, libelle);
|
||||||
|
|||||||
@@ -75,16 +75,16 @@ namespace Webzine.Repository
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Update(Artiste artiste)
|
public void Update(Artiste artiste)
|
||||||
{
|
{
|
||||||
var stored = this.dataStore.Artistes.FirstOrDefault(a => a.IdArtiste == artiste.IdArtiste);
|
Artiste existingArtiste = this.Find(artiste.IdArtiste);
|
||||||
if (stored == null)
|
if (existingArtiste == null)
|
||||||
{
|
{
|
||||||
this.logger.LogWarning("L'artiste {Id} n'a pas été trouvé pour l'update.", artiste.IdArtiste);
|
this.logger.LogWarning("L'artiste {Id} n'a pas été trouvé pour l'update.", artiste.IdArtiste);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
stored.Nom = artiste.Nom;
|
existingArtiste.Nom = artiste.Nom;
|
||||||
stored.Biographie = artiste.Biographie;
|
existingArtiste.Biographie = artiste.Biographie;
|
||||||
stored.Titres = artiste.Titres;
|
existingArtiste.Titres = artiste.Titres;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ namespace Webzine.Repository
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Commentaire Find(int idCommentaire)
|
public Commentaire Find(int idCommentaire)
|
||||||
{
|
{
|
||||||
return this.dataStore.Commentaires.FirstOrDefault(c => c.IdCommentaire == idCommentaire);
|
return this.dataStore.Commentaires.SingleOrDefault(c => c.IdCommentaire == idCommentaire);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -59,13 +59,12 @@ namespace Webzine.Repository
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IEnumerable<Commentaire> Paginate(int offset, int limit)
|
public IEnumerable<Commentaire> FindCommentaires(int offset, int limit)
|
||||||
{
|
{
|
||||||
return this.dataStore.Commentaires
|
return this.dataStore.Commentaires
|
||||||
.OrderByDescending(c => c.DateCreation)
|
.OrderByDescending(c => c.DateCreation)
|
||||||
.Skip(offset)
|
.Skip(offset)
|
||||||
.Take(limit)
|
.Take(limit);
|
||||||
.ToList();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,15 +55,15 @@ public class LocalStyleRepository : IStyleRepository
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Update(Style style)
|
public void Update(Style style)
|
||||||
{
|
{
|
||||||
var stored = this.dataStore.Styles.FirstOrDefault(s => s.IdStyle == style.IdStyle);
|
Style existingStyle = this.Find(style.IdStyle);
|
||||||
if (stored == null)
|
if (existingStyle == null)
|
||||||
{
|
{
|
||||||
this.logger.LogWarning("Style with id {IdStyle} not found for update.", style.IdStyle);
|
this.logger.LogWarning("Style with id {IdStyle} not found for update.", style.IdStyle);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
stored.Libelle = style.Libelle;
|
existingStyle.Libelle = style.Libelle;
|
||||||
stored.Titres = style.Titres;
|
existingStyle.Titres = style.Titres;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ public class LocalTitreRepository : ITitreRepository
|
|||||||
public Titre Find(int idTitre)
|
public Titre Find(int idTitre)
|
||||||
{
|
{
|
||||||
return this.dataStore.Titres
|
return this.dataStore.Titres
|
||||||
.FirstOrDefault(t => t.IdTitre == idTitre);
|
.SingleOrDefault(t => t.IdTitre == idTitre);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -116,18 +116,20 @@ public class LocalTitreRepository : ITitreRepository
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Update(Titre titre)
|
public void Update(Titre titre)
|
||||||
{
|
{
|
||||||
var stored = this.dataStore.Titres.FirstOrDefault(t => t.IdTitre == titre.IdTitre);
|
// On trouve le titre stocké pour mettre à jour ses propriétés avec la méthode Find du repository
|
||||||
if (stored == null)
|
// pour éviter la duplication de code.
|
||||||
|
Titre existingTitre = this.Find(titre.IdTitre);
|
||||||
|
if (existingTitre == null)
|
||||||
{
|
{
|
||||||
this.logger.LogWarning("Titre avec l'ID {Id} non trouvé pour mise à jour.", titre.IdTitre);
|
this.logger.LogWarning("Titre avec l'ID {Id} non trouvé pour mise à jour.", titre.IdTitre);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
stored.Libelle = titre.Libelle;
|
existingTitre.Libelle = titre.Libelle;
|
||||||
stored.DateCreation = titre.DateCreation;
|
existingTitre.DateCreation = titre.DateCreation;
|
||||||
stored.NbLectures = titre.NbLectures;
|
existingTitre.NbLectures = titre.NbLectures;
|
||||||
stored.NbLikes = titre.NbLikes;
|
existingTitre.NbLikes = titre.NbLikes;
|
||||||
stored.IdArtiste = titre.IdArtiste;
|
existingTitre.IdArtiste = titre.IdArtiste;
|
||||||
stored.Styles = titre.Styles;
|
existingTitre.Styles = titre.Styles;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
namespace Webzine.WebApplication.Areas.Administration.Controllers;
|
namespace Webzine.WebApplication.Areas.Administration.Controllers;
|
||||||
|
|
||||||
|
using Business.Contracts;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
||||||
|
using Webzine.Business.Contracts.Dto;
|
||||||
using Webzine.Entity;
|
using Webzine.Entity;
|
||||||
using Webzine.Repository.Contracts;
|
using Webzine.Repository.Contracts;
|
||||||
using Webzine.WebApplication.Areas.Administration.ViewModels.Titre;
|
using Webzine.WebApplication.Areas.Administration.ViewModels.Titre;
|
||||||
@@ -17,6 +20,7 @@ public class TitreController : Controller
|
|||||||
private readonly ITitreRepository titreRepository;
|
private readonly ITitreRepository titreRepository;
|
||||||
private readonly IArtisteRepository artisteRepository;
|
private readonly IArtisteRepository artisteRepository;
|
||||||
private readonly IStyleRepository styleRepository;
|
private readonly IStyleRepository styleRepository;
|
||||||
|
private readonly ITitreAdminService titreAdminService;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="TitreController"/> class.
|
/// Initializes a new instance of the <see cref="TitreController"/> class.
|
||||||
@@ -26,12 +30,14 @@ public class TitreController : Controller
|
|||||||
/// <param name="titreRepository">Repository des titres injecté pour accéder aux données des titres.</param>
|
/// <param name="titreRepository">Repository des titres injecté pour accéder aux données des titres.</param>
|
||||||
/// <param name="artisteRepository">Repository des artistes injecté pour accéder aux données des artistes, nécessaires pour les associations avec les titres.</param>
|
/// <param name="artisteRepository">Repository des artistes injecté pour accéder aux données des artistes, nécessaires pour les associations avec les titres.</param>
|
||||||
/// <param name="styleRepository">Repository des styles injecté pour accéder aux données des styles, nécessaires pour les associations avec les titres.</param>
|
/// <param name="styleRepository">Repository des styles injecté pour accéder aux données des styles, nécessaires pour les associations avec les titres.</param>
|
||||||
public TitreController(ILogger<TitreController> logger, ITitreRepository titreRepository, IArtisteRepository artisteRepository, IStyleRepository styleRepository)
|
/// <param name="titreAdminService">Service Titre Administration injecté gérant Edit et Crée.</param>
|
||||||
|
public TitreController(ILogger<TitreController> logger, ITitreRepository titreRepository, IArtisteRepository artisteRepository, IStyleRepository styleRepository, ITitreAdminService titreAdminService)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.titreRepository = titreRepository;
|
this.titreRepository = titreRepository;
|
||||||
this.artisteRepository = artisteRepository;
|
this.artisteRepository = artisteRepository;
|
||||||
this.styleRepository = styleRepository;
|
this.styleRepository = styleRepository;
|
||||||
|
this.titreAdminService = titreAdminService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -81,6 +87,23 @@ public class TitreController : Controller
|
|||||||
return this.View(model);
|
return this.View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Traite la soumission du formulaire de création d'un titre.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="model">Données saisies dans le formulaire.</param>
|
||||||
|
/// <returns>Redirection vers Index en cas de succès, réaffichage du formulaire sinon.</returns>
|
||||||
|
[HttpPost]
|
||||||
|
public IActionResult Create(TitreAdminDTO model)
|
||||||
|
{
|
||||||
|
if (this.ModelState.IsValid)
|
||||||
|
{
|
||||||
|
this.titreAdminService.CreerTitre(model);
|
||||||
|
return this.RedirectToAction("Index");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.View(model);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Affiche le formulaire de modification d'un titre existant dans la vue Edit, en préremplissant les champs avec les données du titre sélectionné. Les listes déroulantes pour les artistes et les styles sont également remplies pour permettre à l'utilisateur de modifier ces associations.
|
/// Affiche le formulaire de modification d'un titre existant dans la vue Edit, en préremplissant les champs avec les données du titre sélectionné. Les listes déroulantes pour les artistes et les styles sont également remplies pour permettre à l'utilisateur de modifier ces associations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -121,6 +144,23 @@ public class TitreController : Controller
|
|||||||
return this.View(model);
|
return this.View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Traite la soumission du formulaire de modification d'un titre.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="model">Données saisies dans le formulaire.</param>
|
||||||
|
/// <returns>Redirection vers Index en cas de succès, réaffichage du formulaire sinon.</returns>
|
||||||
|
[HttpPost]
|
||||||
|
public IActionResult Edit(TitreAdminDTO model)
|
||||||
|
{
|
||||||
|
if (this.ModelState.IsValid)
|
||||||
|
{
|
||||||
|
this.titreAdminService.ModifierTitre(model);
|
||||||
|
return this.RedirectToAction("Index");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.View(model);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Affiche la vue de confirmation de suppression d'un titre, en récupérant les détails du titre à supprimer à partir de l'identifiant fourni. Le ViewModel contient les informations essentielles du titre, telles que le libellé et le nom de l'artiste, pour permettre à l'utilisateur de confirmer la suppression.
|
/// Affiche la vue de confirmation de suppression d'un titre, en récupérant les détails du titre à supprimer à partir de l'identifiant fourni. Le ViewModel contient les informations essentielles du titre, telles que le libellé et le nom de l'artiste, pour permettre à l'utilisateur de confirmer la suppression.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -163,8 +203,9 @@ public class TitreController : Controller
|
|||||||
if (titre != null)
|
if (titre != null)
|
||||||
{
|
{
|
||||||
this.titreRepository.Delete(titre);
|
this.titreRepository.Delete(titre);
|
||||||
|
return this.RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.RedirectToAction("Index");
|
return this.View(model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
13
Webzine.WebApplication/Configuration/Middlewares.cs
Normal file
13
Webzine.WebApplication/Configuration/Middlewares.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Webzine.WebApplication.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Options de seuil pour la détection des opérations EF Core lentes.
|
||||||
|
/// </summary>
|
||||||
|
public class EfPerformanceOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Obtient ou définit le seuil en millisecondes au-delà duquel une commande SQL est journalisée.
|
||||||
|
/// Valeur par défaut : 200 ms.
|
||||||
|
/// </summary>
|
||||||
|
public int SeuilMs { get; set; } = 200;
|
||||||
|
}
|
||||||
@@ -51,6 +51,8 @@ namespace Webzine.WebApplication.Controllers
|
|||||||
return this.RedirectToAction("Index");
|
return this.RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.titreRepository.IncrementNbLectures(titre);
|
||||||
|
|
||||||
var vm = new TitreDetail
|
var vm = new TitreDetail
|
||||||
{
|
{
|
||||||
Details = new TitreContent
|
Details = new TitreContent
|
||||||
@@ -110,10 +112,11 @@ namespace Webzine.WebApplication.Controllers
|
|||||||
if (titre == null)
|
if (titre == null)
|
||||||
{
|
{
|
||||||
this.logger.LogWarning("Impossible d'ajouter un like. Titre ID {Id} introuvable.", model.IdTitre);
|
this.logger.LogWarning("Impossible d'ajouter un like. Titre ID {Id} introuvable.", model.IdTitre);
|
||||||
return this.RedirectToAction("Index");
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
titre.NbLikes++;
|
{
|
||||||
|
this.titreRepository.IncrementNbLikes(titre);
|
||||||
|
}
|
||||||
|
|
||||||
return this.RedirectToAction("Details", new { id = model.IdTitre });
|
return this.RedirectToAction("Details", new { id = model.IdTitre });
|
||||||
}
|
}
|
||||||
|
|||||||
146
Webzine.WebApplication/Interceptors/EfSlowQueryInterceptor.cs
Normal file
146
Webzine.WebApplication/Interceptors/EfSlowQueryInterceptor.cs
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
namespace Webzine.WebApplication.Interceptors;
|
||||||
|
|
||||||
|
using System.Data.Common;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
using Configuration;
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Intercepteur EF Core qui journalise uniquement les commandes SQL dépassant le seuil configuré.
|
||||||
|
/// Remonte la pile d'appels pour identifier la méthode repository (<c>Webzine.Repository.*</c>) à l'origine de la requête.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// <b>Références :</b>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>
|
||||||
|
/// EF Core interceptors (doc officielle) :
|
||||||
|
/// <see href="https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors"/>
|
||||||
|
/// </item>
|
||||||
|
/// <item>
|
||||||
|
/// <see cref="DbCommandInterceptor"/> API :
|
||||||
|
/// <see href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.diagnostics.dbcommandinterceptor"/>
|
||||||
|
/// </item>
|
||||||
|
/// <item>
|
||||||
|
/// Exemple de slow-query interceptor (SO) :
|
||||||
|
/// <see href="https://medium.com/@sudipdevdev/how-to-detect-and-log-slow-queries-in-entity-framework-core-e2ab71024849"/>
|
||||||
|
/// </item>
|
||||||
|
/// <item>
|
||||||
|
/// <see cref="System.Diagnostics.StackTrace"/> pour remonter l'appelant :
|
||||||
|
/// <see href="https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.stacktrace"/>
|
||||||
|
/// </item>
|
||||||
|
/// <item>
|
||||||
|
/// Enregistrement via <c>AddInterceptors</c> :
|
||||||
|
/// <see href="https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors#registering-interceptors"/>
|
||||||
|
/// </item>
|
||||||
|
/// </list>
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public class EfSlowQueryInterceptor : DbCommandInterceptor
|
||||||
|
{
|
||||||
|
private readonly ILogger<EfSlowQueryInterceptor> logger;
|
||||||
|
private readonly int seuilMs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="EfSlowQueryInterceptor"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Le service de journalisation injecté pour suivre les opérations de l'intercepteur.</param>
|
||||||
|
/// <param name="options">Les options de performance EF injectées pour récupérer le seuil de lenteur configuré.</param>
|
||||||
|
public EfSlowQueryInterceptor(ILogger<EfSlowQueryInterceptor> logger, IOptions<EfPerformanceOptions> options)
|
||||||
|
{
|
||||||
|
this.logger = logger;
|
||||||
|
this.seuilMs = options.Value.SeuilMs;
|
||||||
|
|
||||||
|
this.logger.LogDebug("[EfSlowQueryInterceptor] Constructeur appelé — seuil : {SeuilMs} ms.", this.seuilMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override DbDataReader ReaderExecuted(DbCommand command, CommandExecutedEventData eventData, DbDataReader result)
|
||||||
|
{
|
||||||
|
this.JournaliserSiLent(eventData.Duration);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override ValueTask<DbDataReader> ReaderExecutedAsync(DbCommand command, CommandExecutedEventData eventData, DbDataReader result, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
this.JournaliserSiLent(eventData.Duration);
|
||||||
|
return ValueTask.FromResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override int NonQueryExecuted(DbCommand command, CommandExecutedEventData eventData, int result)
|
||||||
|
{
|
||||||
|
this.JournaliserSiLent(eventData.Duration);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override ValueTask<int> NonQueryExecutedAsync(DbCommand command, CommandExecutedEventData eventData, int result, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
this.JournaliserSiLent(eventData.Duration);
|
||||||
|
return ValueTask.FromResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override object? ScalarExecuted(DbCommand command, CommandExecutedEventData eventData, object? result)
|
||||||
|
{
|
||||||
|
this.JournaliserSiLent(eventData.Duration);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override ValueTask<object?> ScalarExecutedAsync(DbCommand command, CommandExecutedEventData eventData, object? result, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
this.JournaliserSiLent(eventData.Duration);
|
||||||
|
return ValueTask.FromResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remonte la pile d'appels pour trouver la première méthode dans <c>Webzine.Repository</c>.
|
||||||
|
/// Toutes les requêtes EF Core du projet transitent par ce namespace, ce qui garantit
|
||||||
|
/// un résultat pertinent sans parcourir l'intégralité de la stack.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Chaîne <c>Classe.Méthode</c> ou <c>"inconnu"</c> si rien trouvé.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// <see cref="StackTrace"/> est instancié uniquement quand le seuil est dépassé,
|
||||||
|
/// ce qui évite tout impact sur le chemin nominal.
|
||||||
|
/// Ref : <see href="https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.stacktrace"/>.
|
||||||
|
/// </remarks>
|
||||||
|
private static string TrouverAppelantRepository()
|
||||||
|
{
|
||||||
|
// skipFrames: 1 pour sauter TrouverAppelantRepository elle-même
|
||||||
|
// fNeedFileInfo: false — on ne veut pas les numéros de ligne (coût supplémentaire inutile)
|
||||||
|
var frames = new StackTrace(skipFrames: 1, fNeedFileInfo: false).GetFrames();
|
||||||
|
|
||||||
|
foreach (var frame in frames)
|
||||||
|
{
|
||||||
|
var methode = frame.GetMethod();
|
||||||
|
if (methode?.DeclaringType?.Namespace?.StartsWith("Webzine.Repository", StringComparison.Ordinal) == true)
|
||||||
|
{
|
||||||
|
return $"{methode.DeclaringType.Name}.{methode.Name}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "inconnu";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void JournaliserSiLent(TimeSpan duree)
|
||||||
|
{
|
||||||
|
if (duree.TotalMilliseconds > this.seuilMs)
|
||||||
|
{
|
||||||
|
var appelant = TrouverAppelantRepository();
|
||||||
|
|
||||||
|
this.logger.LogWarning(
|
||||||
|
"[EfSlowQueryInterceptor] Opération EF Core lente détectée — durée réelle : {DureeMs} ms — seuil : {SeuilMs} ms — dépassement : +{Depassement} ms.{NewLine}Appelant : {Appelant}",
|
||||||
|
duree.TotalMilliseconds.ToString("F2"),
|
||||||
|
this.seuilMs,
|
||||||
|
(duree.TotalMilliseconds - this.seuilMs).ToString("F2"),
|
||||||
|
Environment.NewLine,
|
||||||
|
appelant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
namespace Webzine.WebApplication.Middlewares
|
||||||
|
{
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
public class LogTempsExecutionMiddleware
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// log à chaque requete http.
|
||||||
|
/// </summary>
|
||||||
|
// _next représente le maillon suivant dans la chaîne (le prochain middleware ou le contrôleur)
|
||||||
|
private readonly RequestDelegate next;
|
||||||
|
private readonly ILogger<LogTempsExecutionMiddleware> logger;
|
||||||
|
|
||||||
|
// Le constructeur récupère "_next" et le Logger
|
||||||
|
public LogTempsExecutionMiddleware(RequestDelegate next, ILogger<LogTempsExecutionMiddleware> logger)
|
||||||
|
{
|
||||||
|
this.next = next;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// méthode appelée à chaque requête HTTP
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Middleware chargé de journaliser le cycle de vie d'une requête HTTP (entrée, exécution, sortie et temps de réponse).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">Le contexte HTTP encapsulant toutes les informations de la requête et de la réponse.</param>
|
||||||
|
/// <returns>Une tâche (<see cref="Task"/>) représentant l'opération asynchrone.</returns>
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
// (Avant le contrôleur)
|
||||||
|
var chronometre = Stopwatch.StartNew(); // lance le chrono
|
||||||
|
|
||||||
|
// --- IN ---
|
||||||
|
var methode = context.Request.Method;
|
||||||
|
var endpoint = context.Request.Path;
|
||||||
|
var traceId = context.TraceIdentifier; // Identifiant unique généré par .NET
|
||||||
|
|
||||||
|
this.logger.LogInformation("[IN] TraceId: {traceId} | Méthode: {methode} | Endpoint: {endpoint}", traceId, methode, endpoint);
|
||||||
|
|
||||||
|
await this.next(context);
|
||||||
|
|
||||||
|
// (Après le contrôleur)
|
||||||
|
chronometre.Stop(); // arrête le chrono
|
||||||
|
var tempsEcoule = chronometre.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
var httpCode = context.Response.StatusCode; // exemple: 200, 404, 500
|
||||||
|
|
||||||
|
// --- OUT ---
|
||||||
|
if (httpCode >= 500)
|
||||||
|
{
|
||||||
|
this.logger.LogError("[OUT] TraceId: {traceId} | HTTP {httpCode} | Temps: {tempsEcoule} ms | Endpoint: {endpoint}", traceId, httpCode, tempsEcoule, endpoint);
|
||||||
|
}
|
||||||
|
else if (httpCode >= 400)
|
||||||
|
{
|
||||||
|
this.logger.LogWarning("[OUT] TraceId: {traceId} | HTTP {httpCode} | Temps: {tempsEcoule} ms | Endpoint: {endpoint}", traceId, httpCode, tempsEcoule, endpoint);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.logger.LogInformation("[OUT] TraceId: {traceId} | HTTP {httpCode} | Temps: {tempsEcoule} ms | Endpoint: {endpoint}", traceId, httpCode, tempsEcoule, endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ using Webzine.Repository;
|
|||||||
using Webzine.Repository.Contracts;
|
using Webzine.Repository.Contracts;
|
||||||
using Webzine.WebApplication.Configuration;
|
using Webzine.WebApplication.Configuration;
|
||||||
using Webzine.WebApplication.Extensions;
|
using Webzine.WebApplication.Extensions;
|
||||||
|
using Webzine.WebApplication.Interceptors;
|
||||||
|
|
||||||
// Initiation du logger NLog pour la classe courante afin de pouvoir l'utiliser pour logger des messages d'information, d'erreur, etc avant la construction de l'application.
|
// Initiation du logger NLog pour la classe courante afin de pouvoir l'utiliser pour logger des messages d'information, d'erreur, etc avant la construction de l'application.
|
||||||
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
||||||
@@ -37,6 +38,12 @@ try
|
|||||||
builder.Logging.ClearProviders();
|
builder.Logging.ClearProviders();
|
||||||
builder.Host.UseNLog();
|
builder.Host.UseNLog();
|
||||||
|
|
||||||
|
builder.Services.Configure<EfPerformanceOptions>(options =>
|
||||||
|
{
|
||||||
|
options.SeuilMs = builder.Configuration.GetValue<int>("EfPerformance:SeuilMs");
|
||||||
|
});
|
||||||
|
builder.Services.AddSingleton<EfSlowQueryInterceptor>();
|
||||||
|
|
||||||
// En fonction de la configuration, utilise soit les repositories basés sur une base de données, soit les repositories basés sur des listes locales.
|
// En fonction de la configuration, utilise soit les repositories basés sur une base de données, soit les repositories basés sur des listes locales.
|
||||||
var repositoryType = builder.Configuration.GetValue<RepositoryType>("Repository");
|
var repositoryType = builder.Configuration.GetValue<RepositoryType>("Repository");
|
||||||
var seederType = builder.Configuration.GetValue<SeederType>("Seeder");
|
var seederType = builder.Configuration.GetValue<SeederType>("Seeder");
|
||||||
@@ -45,13 +52,21 @@ try
|
|||||||
{
|
{
|
||||||
if (builder.Environment.IsProduction())
|
if (builder.Environment.IsProduction())
|
||||||
{
|
{
|
||||||
builder.Services.AddDbContext<WebzineDbContext>(options =>
|
builder.Services.AddDbContext<WebzineDbContext>((serviceProvider, options) =>
|
||||||
options.UseNpgsql(builder.Configuration.GetConnectionString("PostGreSQLConnection")));
|
{
|
||||||
|
options
|
||||||
|
.UseNpgsql(builder.Configuration.GetConnectionString("PostGreSQLConnection"))
|
||||||
|
.AddInterceptors(serviceProvider.GetRequiredService<EfSlowQueryInterceptor>());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
builder.Services.AddDbContext<WebzineDbContext>(options =>
|
builder.Services.AddDbContext<WebzineDbContext>((serviceProvider, options) =>
|
||||||
options.UseSqlite(builder.Configuration.GetConnectionString("SqliteConnection")));
|
{
|
||||||
|
options
|
||||||
|
.UseSqlite(builder.Configuration.GetConnectionString("SqliteConnection"))
|
||||||
|
.AddInterceptors(serviceProvider.GetRequiredService<EfSlowQueryInterceptor>());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.Services.AddScoped<DbEntityRepository>();
|
builder.Services.AddScoped<DbEntityRepository>();
|
||||||
@@ -70,6 +85,7 @@ try
|
|||||||
}
|
}
|
||||||
|
|
||||||
builder.Services.AddScoped<IDashboardService, DashboardService>();
|
builder.Services.AddScoped<IDashboardService, DashboardService>();
|
||||||
|
builder.Services.AddScoped<ITitreAdminService, TitreAdminService>();
|
||||||
|
|
||||||
// https://learn.microsoft.com/fr-fr/aspnet/core/performance/response-compression?view=aspnetcore-10.0#configuration
|
// https://learn.microsoft.com/fr-fr/aspnet/core/performance/response-compression?view=aspnetcore-10.0#configuration
|
||||||
// Ajoute le service de compression des réponses HTTP pour réduire la taille des données envoyées au client et améliorer les performances de l'application.
|
// Ajoute le service de compression des réponses HTTP pour réduire la taille des données envoyées au client et améliorer les performances de l'application.
|
||||||
@@ -77,6 +93,8 @@ try
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseMiddleware<Webzine.WebApplication.Middlewares.LogTempsExecutionMiddleware>();
|
||||||
|
|
||||||
if (repositoryType == RepositoryType.Db)
|
if (repositoryType == RepositoryType.Db)
|
||||||
{
|
{
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
|
|||||||
@@ -13,5 +13,8 @@
|
|||||||
"SqliteConnection": "Data Source=Data/webzine.sqlite",
|
"SqliteConnection": "Data Source=Data/webzine.sqlite",
|
||||||
"PostGreSQLConnection": "Host=localhost;Port=5432;Username=admin;Password=admin123;Database=webzine_db"
|
"PostGreSQLConnection": "Host=localhost;Port=5432;Username=admin;Password=admin123;Database=webzine_db"
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*",
|
||||||
|
"EfPerformance": {
|
||||||
|
"SeuilMs": 10
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
<rules>
|
<rules>
|
||||||
<!-- Vos logs d'application en Debug+ -->
|
<!-- Vos logs d'application en Debug+ -->
|
||||||
<logger name="Webzine.WebApplication.*" minlevel="Debug" writeTo="allfile,ownfile-web,console" />
|
<logger name="Webzine.WebApplication.*" minlevel="Info" writeTo="allfile,ownfile-web,console" />
|
||||||
|
|
||||||
<!-- Logs Microsoft en Warning+ sauf Hosting.Lifetime -->
|
<!-- Logs Microsoft en Warning+ sauf Hosting.Lifetime -->
|
||||||
<logger name="Microsoft.*" minlevel="Warn" writeTo="allfile" final="true" />
|
<logger name="Microsoft.*" minlevel="Warn" writeTo="allfile" final="true" />
|
||||||
|
|||||||
@@ -221,7 +221,4 @@ cat >> "$RAPPORT_FICHIER" <<EOF
|
|||||||
Total : $TOTAL
|
Total : $TOTAL
|
||||||
Réussis : $REUSSIS
|
Réussis : $REUSSIS
|
||||||
Échecs : $ECHECS
|
Échecs : $ECHECS
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Code de sortie non nul si des échecs ont été détectés — permet à la CI de bloquer
|
|
||||||
exit "$ECHECS"
|
|
||||||
Reference in New Issue
Block a user