fix: Dapper underscore-mapping race condition affects AuditSql too, not just TradeSql
The static-ctor guard added to TradeSql in the previous commit was a symptom fix. Confirmed the same bug independently affects AuditSql: running the Compliance test filter in isolation (no other class that happens to touch a BuildingBlocks type first) reproduced the identical failure mode - every snake_case column (event_type, purge_status, ...) silently mapped to null. Root cause: KArtSell.BuildingBlocks.Data.DapperBootstrap's [ModuleInitializer] only runs once that assembly is actually loaded, and a `using` directive for a BuildingBlocks namespace does not force that load - only an executed reference to one of its types does. Any Sql class that never actually touches a BuildingBlocks type at runtime is exposed, and this is a property of *when* a given test/request happens to run relative to everything else in the process, not of any one class. Replaced the ad-hoc TradeSql static ctor with one [ModuleInitializer] per module assembly (KArtSell.Modules.ModelOperations, KArtSell.Modules.SignalEngine). Every Sql/reader class lives inside its own module's assembly, so a module initializer there is guaranteed to run before any of them are used, independent of BuildingBlocks or load order. Verified both KArtSell.Integration.Tests.Compliance and .TradeExecution now pass 100% run in full isolation, not just as part of the full suite. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace KArtSell.Modules.ModelOperations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
internal static class DapperMappingBootstrap
|
||||||
|
{
|
||||||
|
#pragma warning disable CA2255
|
||||||
|
[ModuleInitializer]
|
||||||
|
#pragma warning restore CA2255
|
||||||
|
public static void Initialize()
|
||||||
|
{
|
||||||
|
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,16 +21,6 @@ public class TradeSql : ITradeSql
|
|||||||
private readonly NpgsqlDataSource _dataSource;
|
private readonly NpgsqlDataSource _dataSource;
|
||||||
private readonly ILogger<TradeSql> _logger;
|
private readonly ILogger<TradeSql> _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<TradeSql> logger)
|
public TradeSql(NpgsqlDataSource dataSource, ILogger<TradeSql> logger)
|
||||||
{
|
{
|
||||||
_dataSource = dataSource;
|
_dataSource = dataSource;
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace KArtSell.Modules.SignalEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
internal static class DapperMappingBootstrap
|
||||||
|
{
|
||||||
|
#pragma warning disable CA2255
|
||||||
|
[ModuleInitializer]
|
||||||
|
#pragma warning restore CA2255
|
||||||
|
public static void Initialize()
|
||||||
|
{
|
||||||
|
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user