// Probe LECTURE SEULE ciblé : chargement Sites du snapshot mobile. // // Reproduit EXACTEMENT le chemin du snapshot Sites corrigé : // MobileSnapshotService.LoadSitesAsync // -> SiteService.SearchDetailsAsync // -> BatikSiteRepository.SearchDetailsAsync (bulk, pas de N+1) // // Le probe compte le nombre de requêtes HFSQL exécutées pour prouver que le // chargement est bulk (nombre borné, indépendant du nombre de sites) et // qu'aucun GetById par site n'est émis. // // Aucune écriture. Aucun password affiché. Aucune donnée personnelle loggée. using Microsoft.Extensions.Configuration; using Omika.Core.Contracts.Common; using Omika.Core.Contracts.Sites; using Omika.Core.Domain.Entities.Clients; using Omika.Core.Interfaces; using Omika.Data.Configuration; using Omika.Data.Connection; using Omika.Data.HfSql.Connection; using Omika.Data.HfSql.Metadata; using Omika.Data.Repositories.Batik; // --- 1. Charger la configuration Hfsql (avec FilePasswords) --- var configPath = Path.GetFullPath(Path.Combine( Directory.GetCurrentDirectory(), "config", "hfsql-test.local.json")); var config = new ConfigurationBuilder() .AddJsonFile(configPath, optional: false, reloadOnChange: false) .Build(); var hfsqlOptions = new HfsqlOptions(); config.GetSection(HfsqlOptions.SectionName).Bind(hfsqlOptions); Console.WriteLine($"=== Probe Site Snapshot (base {hfsqlOptions.DatabaseName}) ==="); Console.WriteLine(); // --- 2. Reproduire le mécanisme de l'API : BusinessOleDbConnection --- var factory = new HfSqlOleDbConnectionFactory(); await using var business = new BusinessOleDbConnection(hfsqlOptions, factory); // Wrapper compteur de requêtes HFSQL (lecture seule, délègue tout). var counting = new CountingHfsqlConnection(business); var sw = System.Diagnostics.Stopwatch.StartNew(); try { await counting.OpenAsync(); Console.WriteLine("Connexion OLE DB ouverte (mécanisme API)."); // --- 3. Repository Batik + chemin bulk du snapshot Sites --- ISiteRepository repository = new BatikSiteRepository(counting); var criteria = new SiteSearchCriteria { Page = 1, PageSize = 1000 }; using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); PagedResult result = await repository.SearchDetailsAsync(criteria, cts.Token); sw.Stop(); Console.WriteLine(); Console.WriteLine("--- Résultat ---"); Console.WriteLine($"sites chargés (page) = {result.Items.Count}"); Console.WriteLine($"total sites (SearchDetails) = {result.TotalCount}"); Console.WriteLine($"requêtes HFSQL exécutées = {counting.QueryCount}"); Console.WriteLine($"elapsed = {sw.ElapsedMilliseconds} ms"); // Vérification anti-N+1 : le nombre de requêtes doit être borné et très // inférieur au nombre de sites. Avec l'ancien pattern N+1, on aurait // observé ~1 requête par site (timeout HTTP 30s). if (result.TotalCount > 0 && counting.QueryCount >= result.TotalCount) { Console.WriteLine("ATTENTION : le nombre de requêtes est >= au nombre de sites (pattern N+1 suspect)."); } else { Console.WriteLine("OK : chargement bulk (requêtes << sites), aucun GetById par site."); } } catch (Exception ex) { sw.Stop(); Console.WriteLine($"ERREUR: {ex.Message}"); Console.WriteLine($"elapsed = {sw.ElapsedMilliseconds} ms"); } Console.WriteLine(); Console.WriteLine("Probe Site Snapshot terminé (lecture seule)."); /// /// Wrapper IHfsqlConnection qui délègue à la connexion réelle et compte les /// requêtes de lecture (QueryAsync). Lecture seule : aucune écriture. /// internal sealed class CountingHfsqlConnection : IHfsqlConnection { private readonly IHfsqlConnection _inner; public int QueryCount { get; private set; } public CountingHfsqlConnection(IHfsqlConnection inner) => _inner = inner; public HfsqlOptions Options => _inner.Options; public HfsqlConnectionState State => _inner.State; public Task OpenAsync(CancellationToken ct = default) => _inner.OpenAsync(ct); public Task CloseAsync() => _inner.CloseAsync(); public Task ExecuteScalarAsync(string sql, IReadOnlyList? parameters = null, CancellationToken ct = default) => _inner.ExecuteScalarAsync(sql, parameters, ct); public Task ExecuteNonQueryAsync(string sql, IReadOnlyList? parameters = null, CancellationToken ct = default) => _inner.ExecuteNonQueryAsync(sql, parameters, ct); public async Task>> QueryAsync( string sql, IReadOnlyList? parameters = null, CancellationToken ct = default) { QueryCount++; return await _inner.QueryAsync(sql, parameters, ct); } public Task> GetColumnsAsync(string tableName, CancellationToken ct = default) => _inner.GetColumnsAsync(tableName, ct); public ValueTask DisposeAsync() => _inner.DisposeAsync(); }