refactor(dotnet): split collection read and write contracts
Validators (Pushes and Pull Requests) / validate-ui-and-storage (push) Successful in 17s
Validators (Pushes and Pull Requests) / validate-core (push) Has been cancelled

This commit is contained in:
2026-07-13 01:00:36 +09:00
parent e99c15e6a5
commit e0d278e6eb
6 changed files with 72 additions and 39 deletions
@@ -5,9 +5,9 @@ namespace QuantEngine.Application.Services;
public sealed class CollectionReadModelService : ICollectionReadModelService public sealed class CollectionReadModelService : ICollectionReadModelService
{ {
private readonly ICollectionRepository _repository; private readonly ICollectionReadRepository _repository;
public CollectionReadModelService(ICollectionRepository repository) public CollectionReadModelService(ICollectionReadRepository repository)
{ {
_repository = repository; _repository = repository;
} }
@@ -13,7 +13,8 @@ namespace QuantEngine.Application.Services;
public class KisDataCollectionOrchestrator : ICollectionOrchestrator public class KisDataCollectionOrchestrator : ICollectionOrchestrator
{ {
private readonly IKisApiClient _kisApiClient; private readonly IKisApiClient _kisApiClient;
private readonly ICollectionRepository _repository; private readonly ICollectionWriteRepository _writeRepository;
private readonly ICollectionReadRepository _readRepository;
private readonly PriceDataNormalizer _normalizer; private readonly PriceDataNormalizer _normalizer;
private readonly SourcePriorityResolver _priorityResolver; private readonly SourcePriorityResolver _priorityResolver;
private readonly ILogger<KisDataCollectionOrchestrator> _logger; private readonly ILogger<KisDataCollectionOrchestrator> _logger;
@@ -21,14 +22,16 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator
public KisDataCollectionOrchestrator( public KisDataCollectionOrchestrator(
IKisApiClient kisApiClient, IKisApiClient kisApiClient,
ICollectionRepository repository, ICollectionWriteRepository repository,
ICollectionReadRepository readRepository,
PriceDataNormalizer normalizer, PriceDataNormalizer normalizer,
SourcePriorityResolver priorityResolver, SourcePriorityResolver priorityResolver,
ILogger<KisDataCollectionOrchestrator> logger, ILogger<KisDataCollectionOrchestrator> logger,
IRuntimeAuditTrailService auditTrail) IRuntimeAuditTrailService auditTrail)
{ {
_kisApiClient = kisApiClient; _kisApiClient = kisApiClient;
_repository = repository; _writeRepository = repository;
_readRepository = readRepository;
_normalizer = normalizer; _normalizer = normalizer;
_priorityResolver = priorityResolver; _priorityResolver = priorityResolver;
_logger = logger; _logger = logger;
@@ -66,7 +69,7 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator
CollectionSnapshotRecord? cachedSnapshot = null; CollectionSnapshotRecord? cachedSnapshot = null;
if (IsMarketClosed()) if (IsMarketClosed())
{ {
var latest = await _repository.GetLatestSnapshotsForTickerAsync(ticker, 1); var latest = await _readRepository.GetLatestSnapshotsForTickerAsync(ticker, 1);
var todayPrefix = DateTime.UtcNow.AddHours(9).ToString("yyyy-MM-dd"); var todayPrefix = DateTime.UtcNow.AddHours(9).ToString("yyyy-MM-dd");
if (latest.Count > 0 && latest[0].CapturedAt.StartsWith(todayPrefix)) if (latest.Count > 0 && latest[0].CapturedAt.StartsWith(todayPrefix))
{ {
@@ -96,7 +99,7 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator
} }
// Save to DB // Save to DB
await _repository.SaveSnapshotAsync(new CollectionSnapshotRecord( await _writeRepository.SaveSnapshotAsync(new CollectionSnapshotRecord(
RunId: runId, RunId: runId,
DatasetName: "data_feed", DatasetName: "data_feed",
Ticker: ticker, Ticker: ticker,
@@ -119,7 +122,7 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator
_logger.LogWarning("Skipped invalid OHLCV bar for {Ticker}: constraints not satisfied", ticker); _logger.LogWarning("Skipped invalid OHLCV bar for {Ticker}: constraints not satisfied", ticker);
continue; continue;
} }
await _repository.SavePriceHistoryDailyAsync(priceRecord); await _writeRepository.SavePriceHistoryDailyAsync(priceRecord);
} }
} }
} }
@@ -147,7 +150,7 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator
{ "error_kind", ex.GetType().Name } { "error_kind", ex.GetType().Name }
}); });
await _repository.SaveErrorAsync(new CollectionErrorRecord( await _writeRepository.SaveErrorAsync(new CollectionErrorRecord(
RunId: runId, RunId: runId,
SourceName: "kis_collector", SourceName: "kis_collector",
ErrorKind: ex.GetType().Name, ErrorKind: ex.GetType().Name,
@@ -166,7 +169,7 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator
_auditTrail.Append("collection_audit", runId, new CollectionExecutionAudit(runId, result.Status, DateTimeOffset.Parse(startedAt), DateTimeOffset.Parse(finishedAt), result.SuccessCount, result.ErrorCount, "finished")); _auditTrail.Append("collection_audit", runId, new CollectionExecutionAudit(runId, result.Status, DateTimeOffset.Parse(startedAt), DateTimeOffset.Parse(finishedAt), result.SuccessCount, result.ErrorCount, "finished"));
// Save run record // Save run record
await _repository.SaveRunAsync(new CollectionRunRecord( await _writeRepository.SaveRunAsync(new CollectionRunRecord(
RunId: runId, RunId: runId,
Status: result.Status, Status: result.Status,
StartedAt: startedAt, StartedAt: startedAt,
@@ -364,4 +367,3 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator
} }
} }
} }
@@ -13,7 +13,8 @@ namespace QuantEngine.Core.Tests;
public class KisDataCollectionOrchestratorTests public class KisDataCollectionOrchestratorTests
{ {
private readonly Mock<IKisApiClient> _kisApiClientMock; private readonly Mock<IKisApiClient> _kisApiClientMock;
private readonly Mock<ICollectionRepository> _repositoryMock; private readonly Mock<ICollectionWriteRepository> _writeRepositoryMock;
private readonly Mock<ICollectionReadRepository> _readRepositoryMock;
private readonly Mock<ILogger<KisDataCollectionOrchestrator>> _loggerMock; private readonly Mock<ILogger<KisDataCollectionOrchestrator>> _loggerMock;
private readonly Mock<IRuntimeAuditTrailService> _auditTrailMock; private readonly Mock<IRuntimeAuditTrailService> _auditTrailMock;
private readonly PriceDataNormalizer _normalizer; private readonly PriceDataNormalizer _normalizer;
@@ -23,15 +24,21 @@ public class KisDataCollectionOrchestratorTests
public KisDataCollectionOrchestratorTests() public KisDataCollectionOrchestratorTests()
{ {
_kisApiClientMock = new Mock<IKisApiClient>(); _kisApiClientMock = new Mock<IKisApiClient>();
_repositoryMock = new Mock<ICollectionRepository>(); var repositoryMock = new Mock<ICollectionRepository>();
_writeRepositoryMock = repositoryMock.As<ICollectionWriteRepository>();
_readRepositoryMock = repositoryMock.As<ICollectionReadRepository>();
_loggerMock = new Mock<ILogger<KisDataCollectionOrchestrator>>(); _loggerMock = new Mock<ILogger<KisDataCollectionOrchestrator>>();
_auditTrailMock = new Mock<IRuntimeAuditTrailService>(); _auditTrailMock = new Mock<IRuntimeAuditTrailService>();
_priorityResolver = new SourcePriorityResolver(); _priorityResolver = new SourcePriorityResolver();
_normalizer = new PriceDataNormalizer(_priorityResolver); _normalizer = new PriceDataNormalizer(_priorityResolver);
_kisApiClientMock
.Setup(k => k.GetDailyItemChartPriceAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), "D", It.IsAny<string>()))
.ReturnsAsync(new Dictionary<string, object>());
_orchestrator = new KisDataCollectionOrchestrator( _orchestrator = new KisDataCollectionOrchestrator(
_kisApiClientMock.Object, _kisApiClientMock.Object,
_repositoryMock.Object, _writeRepositoryMock.Object,
_readRepositoryMock.Object,
_normalizer, _normalizer,
_priorityResolver, _priorityResolver,
_loggerMock.Object, _loggerMock.Object,
@@ -56,19 +63,19 @@ public class KisDataCollectionOrchestratorTests
CapturedAt: $"{todayPrefix}T14:30:00" CapturedAt: $"{todayPrefix}T14:30:00"
); );
_repositoryMock _readRepositoryMock
.Setup(r => r.GetLatestSnapshotsForTickerAsync(ticker, It.IsAny<int>())) .Setup(r => r.GetLatestSnapshotsForTickerAsync(ticker, It.IsAny<int>()))
.ReturnsAsync(new List<CollectionSnapshotRecord> { cachedSnapshot }); .ReturnsAsync(new List<CollectionSnapshotRecord> { cachedSnapshot });
_repositoryMock _writeRepositoryMock
.Setup(r => r.SaveSnapshotAsync(It.IsAny<CollectionSnapshotRecord>())) .Setup(r => r.SaveSnapshotAsync(It.IsAny<CollectionSnapshotRecord>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
_repositoryMock _writeRepositoryMock
.Setup(r => r.SavePriceHistoryDailyAsync(It.IsAny<PriceHistoryDailyRecord>())) .Setup(r => r.SavePriceHistoryDailyAsync(It.IsAny<PriceHistoryDailyRecord>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
_repositoryMock _writeRepositoryMock
.Setup(r => r.SaveRunAsync(It.IsAny<CollectionRunRecord>())) .Setup(r => r.SaveRunAsync(It.IsAny<CollectionRunRecord>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
@@ -84,7 +91,7 @@ public class KisDataCollectionOrchestratorTests
"IsMarketClosed should return true and cached snapshot should be used, so KIS API should not be called" "IsMarketClosed should return true and cached snapshot should be used, so KIS API should not be called"
); );
_repositoryMock.Verify( _writeRepositoryMock.Verify(
r => r.SaveSnapshotAsync(It.Is<CollectionSnapshotRecord>(s => r => r.SaveSnapshotAsync(It.Is<CollectionSnapshotRecord>(s =>
s.SourceName.Contains("(Cached)"))), s.SourceName.Contains("(Cached)"))),
Times.Once, Times.Once,
@@ -98,7 +105,7 @@ public class KisDataCollectionOrchestratorTests
var runId = "test-run-002"; var runId = "test-run-002";
var ticker = "005930"; var ticker = "005930";
var account = "mock"; var account = "mock";
_repositoryMock _readRepositoryMock
.Setup(r => r.GetLatestSnapshotsForTickerAsync(ticker, It.IsAny<int>())) .Setup(r => r.GetLatestSnapshotsForTickerAsync(ticker, It.IsAny<int>()))
.ReturnsAsync(new List<CollectionSnapshotRecord>()); .ReturnsAsync(new List<CollectionSnapshotRecord>());
@@ -115,15 +122,15 @@ public class KisDataCollectionOrchestratorTests
.Setup(k => k.GetDailyItemChartPriceAsync(ticker, It.IsAny<string>(), It.IsAny<string>(), "D", account)) .Setup(k => k.GetDailyItemChartPriceAsync(ticker, It.IsAny<string>(), It.IsAny<string>(), "D", account))
.ReturnsAsync(new Dictionary<string, object>()); .ReturnsAsync(new Dictionary<string, object>());
_repositoryMock _writeRepositoryMock
.Setup(r => r.SaveSnapshotAsync(It.IsAny<CollectionSnapshotRecord>())) .Setup(r => r.SaveSnapshotAsync(It.IsAny<CollectionSnapshotRecord>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
_repositoryMock _writeRepositoryMock
.Setup(r => r.SavePriceHistoryDailyAsync(It.IsAny<PriceHistoryDailyRecord>())) .Setup(r => r.SavePriceHistoryDailyAsync(It.IsAny<PriceHistoryDailyRecord>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
_repositoryMock _writeRepositoryMock
.Setup(r => r.SaveRunAsync(It.IsAny<CollectionRunRecord>())) .Setup(r => r.SaveRunAsync(It.IsAny<CollectionRunRecord>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
@@ -158,7 +165,7 @@ public class KisDataCollectionOrchestratorTests
CapturedAt: $"{priorDay}T14:30:00" CapturedAt: $"{priorDay}T14:30:00"
); );
_repositoryMock _readRepositoryMock
.Setup(r => r.GetLatestSnapshotsForTickerAsync(ticker, It.IsAny<int>())) .Setup(r => r.GetLatestSnapshotsForTickerAsync(ticker, It.IsAny<int>()))
.ReturnsAsync(new List<CollectionSnapshotRecord> { priorDaySnapshot }); .ReturnsAsync(new List<CollectionSnapshotRecord> { priorDaySnapshot });
@@ -174,15 +181,15 @@ public class KisDataCollectionOrchestratorTests
.Setup(k => k.GetDailyItemChartPriceAsync(ticker, It.IsAny<string>(), It.IsAny<string>(), "D", account)) .Setup(k => k.GetDailyItemChartPriceAsync(ticker, It.IsAny<string>(), It.IsAny<string>(), "D", account))
.ReturnsAsync(new Dictionary<string, object>()); .ReturnsAsync(new Dictionary<string, object>());
_repositoryMock _writeRepositoryMock
.Setup(r => r.SaveSnapshotAsync(It.IsAny<CollectionSnapshotRecord>())) .Setup(r => r.SaveSnapshotAsync(It.IsAny<CollectionSnapshotRecord>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
_repositoryMock _writeRepositoryMock
.Setup(r => r.SavePriceHistoryDailyAsync(It.IsAny<PriceHistoryDailyRecord>())) .Setup(r => r.SavePriceHistoryDailyAsync(It.IsAny<PriceHistoryDailyRecord>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
_repositoryMock _writeRepositoryMock
.Setup(r => r.SaveRunAsync(It.IsAny<CollectionRunRecord>())) .Setup(r => r.SaveRunAsync(It.IsAny<CollectionRunRecord>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
@@ -206,7 +213,7 @@ public class KisDataCollectionOrchestratorTests
var ticker = "005930"; var ticker = "005930";
var account = "mock"; var account = "mock";
_repositoryMock _readRepositoryMock
.Setup(r => r.GetLatestSnapshotsForTickerAsync(ticker, It.IsAny<int>())) .Setup(r => r.GetLatestSnapshotsForTickerAsync(ticker, It.IsAny<int>()))
.ReturnsAsync(new List<CollectionSnapshotRecord>()); .ReturnsAsync(new List<CollectionSnapshotRecord>());
@@ -222,15 +229,15 @@ public class KisDataCollectionOrchestratorTests
.Setup(k => k.GetDailyItemChartPriceAsync(ticker, It.IsAny<string>(), It.IsAny<string>(), "D", account)) .Setup(k => k.GetDailyItemChartPriceAsync(ticker, It.IsAny<string>(), It.IsAny<string>(), "D", account))
.ReturnsAsync(new Dictionary<string, object>()); .ReturnsAsync(new Dictionary<string, object>());
_repositoryMock _writeRepositoryMock
.Setup(r => r.SaveSnapshotAsync(It.IsAny<CollectionSnapshotRecord>())) .Setup(r => r.SaveSnapshotAsync(It.IsAny<CollectionSnapshotRecord>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
_repositoryMock _writeRepositoryMock
.Setup(r => r.SavePriceHistoryDailyAsync(It.IsAny<PriceHistoryDailyRecord>())) .Setup(r => r.SavePriceHistoryDailyAsync(It.IsAny<PriceHistoryDailyRecord>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
_repositoryMock _writeRepositoryMock
.Setup(r => r.SaveRunAsync(It.IsAny<CollectionRunRecord>())) .Setup(r => r.SaveRunAsync(It.IsAny<CollectionRunRecord>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
@@ -261,7 +268,7 @@ public class KisDataCollectionOrchestratorTests
var account = "mock"; var account = "mock";
var tickers = new List<string> { "005930", "000660" }; var tickers = new List<string> { "005930", "000660" };
_repositoryMock _readRepositoryMock
.Setup(r => r.GetLatestSnapshotsForTickerAsync(It.IsAny<string>(), It.IsAny<int>())) .Setup(r => r.GetLatestSnapshotsForTickerAsync(It.IsAny<string>(), It.IsAny<int>()))
.ReturnsAsync(new List<CollectionSnapshotRecord>()); .ReturnsAsync(new List<CollectionSnapshotRecord>());
@@ -286,7 +293,7 @@ public class KisDataCollectionOrchestratorTests
.ReturnsAsync(new Dictionary<string, object>()); .ReturnsAsync(new Dictionary<string, object>());
var callCount = 0; var callCount = 0;
_repositoryMock _writeRepositoryMock
.Setup(r => r.SaveSnapshotAsync(It.IsAny<CollectionSnapshotRecord>())) .Setup(r => r.SaveSnapshotAsync(It.IsAny<CollectionSnapshotRecord>()))
.Returns((CollectionSnapshotRecord snapshot) => .Returns((CollectionSnapshotRecord snapshot) =>
{ {
@@ -296,15 +303,15 @@ public class KisDataCollectionOrchestratorTests
return Task.CompletedTask; return Task.CompletedTask;
}); });
_repositoryMock _writeRepositoryMock
.Setup(r => r.SavePriceHistoryDailyAsync(It.IsAny<PriceHistoryDailyRecord>())) .Setup(r => r.SavePriceHistoryDailyAsync(It.IsAny<PriceHistoryDailyRecord>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
_repositoryMock _writeRepositoryMock
.Setup(r => r.SaveErrorAsync(It.IsAny<CollectionErrorRecord>())) .Setup(r => r.SaveErrorAsync(It.IsAny<CollectionErrorRecord>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
_repositoryMock _writeRepositoryMock
.Setup(r => r.SaveRunAsync(It.IsAny<CollectionRunRecord>())) .Setup(r => r.SaveRunAsync(It.IsAny<CollectionRunRecord>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
@@ -315,7 +322,7 @@ public class KisDataCollectionOrchestratorTests
Assert.Equal(1, result.SuccessCount); Assert.Equal(1, result.SuccessCount);
Assert.Equal(1, result.ErrorCount); Assert.Equal(1, result.ErrorCount);
_repositoryMock.Verify( _writeRepositoryMock.Verify(
r => r.SaveErrorAsync(It.Is<CollectionErrorRecord>(e => r => r.SaveErrorAsync(It.Is<CollectionErrorRecord>(e =>
e.Ticker == "000660" && e.ErrorMessage == "Storage Error")), e.Ticker == "000660" && e.ErrorMessage == "Storage Error")),
Times.Once Times.Once
@@ -411,3 +418,5 @@ public class KisDataCollectionOrchestratorTests
throw new InvalidOperationException("Repository root not found."); throw new InvalidOperationException("Repository root not found.");
} }
} }
@@ -44,6 +44,25 @@ public interface IDataCollectionStore
Task<CollectionDashboardStateRecord> GetDashboardStateAsync(); Task<CollectionDashboardStateRecord> GetDashboardStateAsync();
} }
public interface ICollectionWriteRepository
{
Task SaveRunAsync(CollectionRunRecord run);
Task UpdateRunStatusAsync(string runId, string status, string? finishedAt = null, int? totalSnapshots = null, int? totalErrors = null);
Task SaveSnapshotAsync(CollectionSnapshotRecord snapshot);
Task SaveErrorAsync(CollectionErrorRecord error);
Task SavePriceHistoryDailyAsync(PriceHistoryDailyRecord record);
}
public interface ICollectionReadRepository
{
Task<List<CollectionRunRecord>> GetRecentRunsAsync(int limit = 20);
Task<List<CollectionSnapshotRecord>> GetRunSnapshotsAsync(string runId);
Task<List<CollectionErrorRecord>> GetRunErrorsAsync(string runId, int limit = 50);
Task<CollectionDashboardStateRecord> GetDashboardStateAsync();
Task<List<CollectionSnapshotRecord>> GetLatestSnapshotsForTickerAsync(string ticker, int limit = 10);
Task<List<PriceHistorySummaryRecord>> GetPriceHistorySummaryAsync();
}
/// <summary> /// <summary>
/// Collection run record (maps Python CollectionRun). /// Collection run record (maps Python CollectionRun).
/// </summary> /// </summary>
@@ -8,7 +8,7 @@ using QuantEngine.Infrastructure.Data;
namespace QuantEngine.Infrastructure.Repositories namespace QuantEngine.Infrastructure.Repositories
{ {
public class CollectionRepository : ICollectionRepository public class CollectionRepository : ICollectionRepository, ICollectionReadRepository, ICollectionWriteRepository
{ {
private readonly IDbConnectionFactory _connectionFactory; private readonly IDbConnectionFactory _connectionFactory;
+4 -1
View File
@@ -107,7 +107,10 @@ try
builder.Services.AddScoped<JsonSeedIngestionService>(); builder.Services.AddScoped<JsonSeedIngestionService>();
builder.Services.AddScoped<IPostgresqlHistorySnapshotReader, PostgresqlHistorySnapshotReader>(); builder.Services.AddScoped<IPostgresqlHistorySnapshotReader, PostgresqlHistorySnapshotReader>();
builder.Services.AddScoped<HistoryIngestionService>(); builder.Services.AddScoped<HistoryIngestionService>();
builder.Services.AddScoped<ICollectionRepository, CollectionRepository>(); builder.Services.AddScoped<CollectionRepository>();
builder.Services.AddScoped<ICollectionRepository>(sp => sp.GetRequiredService<CollectionRepository>());
builder.Services.AddScoped<ICollectionReadRepository>(sp => sp.GetRequiredService<CollectionRepository>());
builder.Services.AddScoped<ICollectionWriteRepository>(sp => sp.GetRequiredService<CollectionRepository>());
builder.Services.AddScoped<ICollectionReadModelService, CollectionReadModelService>(); builder.Services.AddScoped<ICollectionReadModelService, CollectionReadModelService>();
builder.Services.AddSingleton<IRuntimeAuditTrailService, RuntimeAuditTrailService>(); builder.Services.AddSingleton<IRuntimeAuditTrailService, RuntimeAuditTrailService>();
builder.Services.AddScoped<ITokenCache, PostgresTokenCache>(); builder.Services.AddScoped<ITokenCache, PostgresTokenCache>();