R3 Slice 3: CongressTrading-Modul + Worker-Log + core_settings auf EF Core

- Modul: CongressTradingDbContext (ct_congressMember/ct_trade) + Design-Time-Factory;
  CongressRepository von Dapper auf EF; CongressTrade.DetailsFetched ergaenzt; Dapper entfernt
- Core: CoreSettingsService (core_settings via EF); WorkerBase-Log auf EF (core_worker_log);
  7 Worker-Ctors DatabaseService -> IDbContextFactory<CoreDbContext>
- CoreMigrations + CongressMigrations (Dapper) entfernt; core_-Schema nun rein EF
- EF-Migration InitialCongressTrading; AddCorePersistence-Fallback fuer leeren Connection-String
  (App startet ohne DB); Connection aus appsettings.Local.json (gitignored)
- Tests: +4 CongressRepository (EF-InMemory) -> 39/39 gruen; Build + smoke-ui + App-Start ok

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
This commit is contained in:
Richard
2026-07-28 11:34:42 +02:00
parent 7e3791b82d
commit 6c8e36dc3c
27 changed files with 832 additions and 323 deletions
@@ -1,3 +1,5 @@
using IBKRTrader.Core.Persistence.Ef;
using Microsoft.EntityFrameworkCore;
using IBKRTrader.Core.Database;
using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Settings;
@@ -21,7 +23,7 @@ public class BackupWorker : WorkerBase
protected override TimeSpan? Interval =>
TimeSpan.FromMinutes(_settings.Settings.WorkerSettings.BackupWorker.IntervalMinutes);
public BackupWorker(LoggingService logger, DatabaseService db, SettingsService settings)
public BackupWorker(LoggingService logger, IDbContextFactory<CoreDbContext> db, SettingsService settings)
: base(logger, db)
{
_settings = settings;
@@ -1,3 +1,5 @@
using IBKRTrader.Core.Persistence.Ef;
using Microsoft.EntityFrameworkCore;
using IBKRTrader.Core.Database;
using IBKRTrader.Core.IBKR;
using IBKRTrader.Core.Logging;
@@ -22,7 +24,7 @@ public class IBKRInstrumentSyncWorker : WorkerBase
private readonly IBKRMarketDataRepository _repo;
public IBKRInstrumentSyncWorker(
LoggingService logger, DatabaseService db,
LoggingService logger, IDbContextFactory<CoreDbContext> db,
SettingsService settings, IBKRGatewayService gateway,
IBKRMarketDataRepository repo)
: base(logger, db)
@@ -1,3 +1,5 @@
using IBKRTrader.Core.Persistence.Ef;
using Microsoft.EntityFrameworkCore;
using IBKRTrader.Core.Database;
using IBKRTrader.Core.IBKR;
using IBKRTrader.Core.Logging;
@@ -29,7 +31,7 @@ public class IBKRPriceHistoryWorker : WorkerBase
private readonly IBKRMarketDataRepository _repo;
public IBKRPriceHistoryWorker(
LoggingService logger, DatabaseService db,
LoggingService logger, IDbContextFactory<CoreDbContext> db,
SettingsService settings, IBKRGatewayService gateway,
IBKRMarketDataRepository repo)
: base(logger, db)
@@ -1,3 +1,5 @@
using IBKRTrader.Core.Persistence.Ef;
using Microsoft.EntityFrameworkCore;
using System.Net;
using System.Text;
using System.Text.Json;
@@ -27,7 +29,7 @@ public class WebApiService : WorkerBase
protected override TimeSpan? Interval => null;
public WebApiService(LoggingService logger, DatabaseService db, SettingsService settings)
public WebApiService(LoggingService logger, IDbContextFactory<CoreDbContext> db, SettingsService settings)
: base(logger, db)
{
_settings = settings;
@@ -1,3 +1,5 @@
using IBKRTrader.Core.Persistence.Ef;
using Microsoft.EntityFrameworkCore;
using System.Net;
using System.Text;
using IBKRTrader.Core.Database;
@@ -22,7 +24,7 @@ public class WebserverService : WorkerBase
protected override TimeSpan? Interval => null; // Service = permanent
public WebserverService(LoggingService logger, DatabaseService db, SettingsService settings)
public WebserverService(LoggingService logger, IDbContextFactory<CoreDbContext> db, SettingsService settings)
: base(logger, db)
{
_settings = settings;
+32 -10
View File
@@ -1,5 +1,7 @@
using IBKRTrader.Core.Database;
using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Persistence.Ef;
using IBKRTrader.Core.Persistence.Entities;
using Microsoft.EntityFrameworkCore;
namespace IBKRTrader.Core.Workers;
@@ -26,8 +28,8 @@ public abstract class WorkerBase : IWorker
// ─── Infrastruktur ────────────────────────────────────────────────────────
protected readonly LoggingService Logger;
protected readonly DatabaseService Db;
protected readonly LoggingService Logger;
private readonly IDbContextFactory<CoreDbContext> _dbf;
public WorkerInfo Info { get; } = new();
@@ -35,10 +37,10 @@ public abstract class WorkerBase : IWorker
private Task? _runLoop;
private readonly SemaphoreSlim _triggerSemaphore = new(0, 1);
protected WorkerBase(LoggingService logger, DatabaseService db)
protected WorkerBase(LoggingService logger, IDbContextFactory<CoreDbContext> dbf)
{
Logger = logger;
Db = db;
_dbf = dbf;
Info.WorkerName = Name;
Info.Module = Module;
@@ -153,13 +155,33 @@ public abstract class WorkerBase : IWorker
// ─── DB-Log-Seam (überschreibbar für Unit-Tests) ──────────────────────────
/// <summary>Legt den Worker-Log-Eintrag an. Kapselt den DB-Zugriff (testbar).</summary>
protected virtual Task<long> BeginRunLogAsync()
=> Db.BeginWorkerLogAsync(Name, Module);
/// <summary>Legt den Worker-Log-Eintrag an (core_worker_log via EF). Kapselt den DB-Zugriff (testbar).</summary>
protected virtual async Task<long> BeginRunLogAsync()
{
await using var db = await _dbf.CreateDbContextAsync();
var log = new CoreWorkerLog
{
WorkerName = Name,
Module = Module,
StartedAt = DateTime.UtcNow,
Status = "Running"
};
db.WorkerLog.Add(log);
await db.SaveChangesAsync();
return log.Id;
}
/// <summary>Schließt den Worker-Log-Eintrag ab. Kapselt den DB-Zugriff (testbar).</summary>
protected virtual Task EndRunLogAsync(long logId, bool success, string? message = null)
=> Db.EndWorkerLogAsync(logId, success, message);
protected virtual async Task EndRunLogAsync(long logId, bool success, string? message = null)
{
await using var db = await _dbf.CreateDbContextAsync();
var log = await db.WorkerLog.FindAsync(logId);
if (log is null) return;
log.FinishedAt = DateTime.UtcNow;
log.Status = success ? "Success" : "Error";
log.Message = message;
await db.SaveChangesAsync();
}
// ─── Hilfsmethoden ────────────────────────────────────────────────────────