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)