diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs index 5ab979a8..bb51d155 100644 --- a/Source/AppScaffolding/LibationScaffolding.cs +++ b/Source/AppScaffolding/LibationScaffolding.cs @@ -225,6 +225,7 @@ public static class LibationScaffolding private static void configureLogging(Configuration config) { config.ConfigureLogging(); + DbContexts.TryEmitPendingInitialDatabaseStatistics(); // capture most Console.WriteLine() and write to serilog. See below tests for details. // Some dependencies print helpful info via Console.WriteLine. We'd like to log it. diff --git a/Source/ApplicationServices/DbContexts.cs b/Source/ApplicationServices/DbContexts.cs index 03f3538b..f4a468e0 100644 --- a/Source/ApplicationServices/DbContexts.cs +++ b/Source/ApplicationServices/DbContexts.cs @@ -1,6 +1,9 @@ using DataLayer; using LibationFileManager; +using Serilog; +using System; using System.Collections.Generic; +using System.Threading; namespace ApplicationServices; @@ -8,6 +11,24 @@ public static class DbContexts { private static bool _sqliteDbValidated; + private static readonly object _initialDatabaseStatisticsCaptureLock = new(); + + /// + /// True after initial DB statistics were read and either written to Serilog or stored for . + /// False if capture has not run yet or the last attempt threw (a later may retry). + /// + private static bool _initialDatabaseStatisticsCaptured; + + /// Shape of the initial DB statistics log event; edit here only when changing what is logged. + private sealed class InitialDatabaseStatistics + { + public required int LibraryBooksNotInTrash { get; init; } + public required int LibraryBooksInTrash { get; init; } + public required int BookRecords { get; init; } + } + + private static InitialDatabaseStatistics? _pendingInitialDbStats; + /// Use for fully functional context, incl. SaveChanges(). For query-only, use the other method public static LibationContext GetContext() { @@ -25,9 +46,59 @@ public static class DbContexts _sqliteDbValidated = true; } + TryCaptureInitialDatabaseStatistics(context); + return context; } + private static void TryCaptureInitialDatabaseStatistics(LibationContext context) + { + lock (_initialDatabaseStatisticsCaptureLock) + { + if (_initialDatabaseStatisticsCaptured) + return; + + try + { + var (notInTrash, inTrash) = context.GetLibraryBookCountsByTrashFlag(); + var bookRecords = context.GetBookCount(); + + var stats = new InitialDatabaseStatistics + { + LibraryBooksNotInTrash = notInTrash, + LibraryBooksInTrash = inTrash, + BookRecords = bookRecords, + }; + + if (Configuration.Instance.SerilogInitialized) + LogInitialDatabaseStatistics(stats); + else + _pendingInitialDbStats = stats; + + _initialDatabaseStatisticsCaptured = true; + } + catch (Exception ex) + { + if (Configuration.Instance.SerilogInitialized) + Log.Warning(ex, "Could not capture initial database statistics"); + } + } + } + + /// + /// Writes initial DB statistics that were captured before Serilog was configured (e.g. WinForms early library load). + /// Call once after . + /// + public static void TryEmitPendingInitialDatabaseStatistics() + { + var pending = Interlocked.Exchange(ref _pendingInitialDbStats, null); + if (pending is not null) + LogInitialDatabaseStatistics(pending); + } + + private static void LogInitialDatabaseStatistics(InitialDatabaseStatistics stats) => + Log.Logger.Information("Initial database statistics. {@DbStats}", stats); + /// Use for full library querying. No lazy loading public static List GetLibrary_Flat_NoTracking(bool includeParents = false) { diff --git a/Source/DataLayer/QueryObjects/BookQueries.cs b/Source/DataLayer/QueryObjects/BookQueries.cs index 1353fc9e..6a4f1084 100644 --- a/Source/DataLayer/QueryObjects/BookQueries.cs +++ b/Source/DataLayer/QueryObjects/BookQueries.cs @@ -16,6 +16,10 @@ public static class BookQueries .AsNoTrackingWithIdentityResolution() .GetBook(productId); + /// Total rows in (no related entities loaded). + public static int GetBookCount(this LibationContext context) + => context.Books.AsNoTracking().Count(); + public static Book? GetBook(this IQueryable books, string productId) => books .GetBooks() diff --git a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs index e70be80f..93d29199 100644 --- a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs +++ b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs @@ -62,6 +62,13 @@ public static class LibraryBookQueries .Where(lb => lb.IsDeleted || lb.Book.ContentType == ContentType.Parent) .getLibrary() .ToList(); + + /// Counts rows by (no related entities loaded). + public (int NotInTrash, int InTrash) GetLibraryBookCountsByTrashFlag() + { + var q = context.LibraryBooks.AsNoTracking(); + return (q.Count(lb => !lb.IsDeleted), q.Count(lb => lb.IsDeleted)); + } } extension(IQueryable library)