diff --git a/src/KArtSell.Modules.ModelOperations/DapperMappingBootstrap.cs b/src/KArtSell.Modules.ModelOperations/DapperMappingBootstrap.cs
new file mode 100644
index 00000000..55943901
--- /dev/null
+++ b/src/KArtSell.Modules.ModelOperations/DapperMappingBootstrap.cs
@@ -0,0 +1,25 @@
+using System.Runtime.CompilerServices;
+
+namespace KArtSell.Modules.ModelOperations;
+
+///
+/// KArtSell.BuildingBlocks.Data.DapperBootstrap sets Dapper's snake_case-to-PascalCase column
+/// mapping via its own [ModuleInitializer], but that only fires once that assembly is actually
+/// loaded into the process. Several Sql classes in this module (e.g. AuditSql, TradeSql) only
+/// have a `using` for a BuildingBlocks namespace without ever touching a type from it at
+/// runtime, so under test isolation - or any host that queries this module before touching
+/// BuildingBlocks - the load (and the mapping) can be skipped, silently nulling out every
+/// snake_case column. Every Sql class in this assembly is defined here, so a module initializer
+/// in this assembly is guaranteed to run before any of them are used, regardless of what else
+/// has loaded.
+///
+internal static class DapperMappingBootstrap
+{
+#pragma warning disable CA2255
+ [ModuleInitializer]
+#pragma warning restore CA2255
+ public static void Initialize()
+ {
+ Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
+ }
+}
diff --git a/src/KArtSell.Modules.ModelOperations/TradeExecution/TradeSql.cs b/src/KArtSell.Modules.ModelOperations/TradeExecution/TradeSql.cs
index b1df1a00..a33dc955 100644
--- a/src/KArtSell.Modules.ModelOperations/TradeExecution/TradeSql.cs
+++ b/src/KArtSell.Modules.ModelOperations/TradeExecution/TradeSql.cs
@@ -21,16 +21,6 @@ public class TradeSql : ITradeSql
private readonly NpgsqlDataSource _dataSource;
private readonly ILogger _logger;
- static TradeSql()
- {
- // KArtSell.BuildingBlocks.Data.DapperBootstrap sets this via [ModuleInitializer], but that
- // only fires once its assembly is actually loaded into the process. Nothing in this class
- // references a BuildingBlocks type, so under test isolation (or any host that queries Trade
- // before touching BuildingBlocks) that assembly load - and the mapping - can be skipped,
- // silently nulling out every snake_case column (kis_order_id, sell_decision_id, ...).
- Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
- }
-
public TradeSql(NpgsqlDataSource dataSource, ILogger logger)
{
_dataSource = dataSource;
diff --git a/src/KArtSell.Modules.SignalEngine/DapperMappingBootstrap.cs b/src/KArtSell.Modules.SignalEngine/DapperMappingBootstrap.cs
new file mode 100644
index 00000000..f7f6ce1f
--- /dev/null
+++ b/src/KArtSell.Modules.SignalEngine/DapperMappingBootstrap.cs
@@ -0,0 +1,21 @@
+using System.Runtime.CompilerServices;
+
+namespace KArtSell.Modules.SignalEngine;
+
+///
+/// See KArtSell.Modules.ModelOperations.DapperMappingBootstrap for the full rationale: Dapper's
+/// snake_case-to-PascalCase column mapping is a process-wide static flag set via
+/// KArtSell.BuildingBlocks.Data.DapperBootstrap's [ModuleInitializer], which only fires once
+/// that assembly is loaded. This mirrors it locally so every Sql/reader class defined in this
+/// assembly is guaranteed the mapping is on before its first query, regardless of load order.
+///
+internal static class DapperMappingBootstrap
+{
+#pragma warning disable CA2255
+ [ModuleInitializer]
+#pragma warning restore CA2255
+ public static void Initialize()
+ {
+ Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
+ }
+}