namespace Webzine.WebApplication.Interceptors; using System.Data.Common; using System.Diagnostics; using Configuration; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Options; /// /// 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 (Webzine.Repository.*) à l'origine de la requête. /// /// /// /// Références : /// /// /// EF Core interceptors (doc officielle) : /// /// /// /// API : /// /// /// /// Exemple de slow-query interceptor (SO) : /// /// /// /// pour remonter l'appelant : /// /// /// /// Enregistrement via AddInterceptors : /// /// /// /// /// public class EfSlowQueryInterceptor : DbCommandInterceptor { private readonly ILogger logger; private readonly int seuilMs; /// /// Initializes a new instance of the class. /// /// Le service de journalisation injecté pour suivre les opérations de l'intercepteur. /// Les options de performance EF injectées pour récupérer le seuil de lenteur configuré. public EfSlowQueryInterceptor(ILogger logger, IOptions options) { this.logger = logger; this.seuilMs = options.Value.SeuilMs; this.logger.LogDebug("[EfSlowQueryInterceptor] Constructeur appelé — seuil : {SeuilMs} ms.", this.seuilMs); } /// public override DbDataReader ReaderExecuted(DbCommand command, CommandExecutedEventData eventData, DbDataReader result) { this.JournaliserSiLent(eventData.Duration); return result; } /// public override ValueTask ReaderExecutedAsync(DbCommand command, CommandExecutedEventData eventData, DbDataReader result, CancellationToken cancellationToken = default) { this.JournaliserSiLent(eventData.Duration); return ValueTask.FromResult(result); } /// public override int NonQueryExecuted(DbCommand command, CommandExecutedEventData eventData, int result) { this.JournaliserSiLent(eventData.Duration); return result; } /// public override ValueTask NonQueryExecutedAsync(DbCommand command, CommandExecutedEventData eventData, int result, CancellationToken cancellationToken = default) { this.JournaliserSiLent(eventData.Duration); return ValueTask.FromResult(result); } /// public override object? ScalarExecuted(DbCommand command, CommandExecutedEventData eventData, object? result) { this.JournaliserSiLent(eventData.Duration); return result; } /// public override ValueTask ScalarExecutedAsync(DbCommand command, CommandExecutedEventData eventData, object? result, CancellationToken cancellationToken = default) { this.JournaliserSiLent(eventData.Duration); return ValueTask.FromResult(result); } /// /// Remonte la pile d'appels pour trouver la première méthode dans Webzine.Repository. /// 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. /// /// Chaîne Classe.Méthode ou "inconnu" si rien trouvé. /// /// est instancié uniquement quand le seuil est dépassé, /// ce qui évite tout impact sur le chemin nominal. /// Ref : . /// 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); } } }