@
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:
@@ -1,91 +0,0 @@
|
||||
using IBKRTrader.Core.Logging;
|
||||
|
||||
namespace IBKRTrader.Core.Database.Migrations;
|
||||
|
||||
/// <summary>
|
||||
/// Erstellt alle core_xxx-Tabellen idempotent (IF NOT EXISTS).
|
||||
/// Wird einmalig beim App-Start ausgeführt.
|
||||
/// </summary>
|
||||
public class CoreMigrations
|
||||
{
|
||||
private readonly DatabaseService _db;
|
||||
private readonly LoggingService _logger;
|
||||
|
||||
public CoreMigrations(DatabaseService db, LoggingService logger)
|
||||
{
|
||||
_db = db;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task RunAsync()
|
||||
{
|
||||
_logger.Info("Core", "Starte Core-Datenbankmigrationen...");
|
||||
await CreateCoreSettingsAsync();
|
||||
await CreateCoreWorkerLogAsync();
|
||||
await CreateCoreTradeHistoryAsync();
|
||||
await CreateCoreBudgetAsync();
|
||||
await CreateCorePositionAsync();
|
||||
_logger.Info("Core", "Core-Migrationen abgeschlossen.");
|
||||
}
|
||||
|
||||
// ─── Tabellen ─────────────────────────────────────────────────────────────
|
||||
|
||||
private Task CreateCoreSettingsAsync() => _db.ExecuteAsync(@"
|
||||
CREATE TABLE IF NOT EXISTS `core_settings` (
|
||||
`key` VARCHAR(100) NOT NULL PRIMARY KEY,
|
||||
`value` TEXT,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
|
||||
private Task CreateCoreWorkerLogAsync() => _db.ExecuteAsync(@"
|
||||
CREATE TABLE IF NOT EXISTS `core_worker_log` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`worker_name` VARCHAR(100) NOT NULL,
|
||||
`module` VARCHAR(50) NOT NULL DEFAULT 'Core',
|
||||
`started_at` DATETIME,
|
||||
`finished_at` DATETIME,
|
||||
`status` ENUM('Running','Success','Error') DEFAULT 'Running',
|
||||
`message` TEXT,
|
||||
INDEX `idx_worker` (`worker_name`, `started_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
|
||||
private Task CreateCoreTradeHistoryAsync() => _db.ExecuteAsync(@"
|
||||
CREATE TABLE IF NOT EXISTS `core_trade_history` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`module` VARCHAR(50) NOT NULL,
|
||||
`symbol` VARCHAR(20) NOT NULL,
|
||||
`action` ENUM('BUY','SELL') NOT NULL,
|
||||
`quantity` DECIMAL(18,4),
|
||||
`price` DECIMAL(18,4),
|
||||
`total_value` DECIMAL(18,4),
|
||||
`traded_at` DATETIME,
|
||||
`ibkr_order_id` VARCHAR(100),
|
||||
`status` VARCHAR(50),
|
||||
`notes` TEXT,
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX `idx_symbol` (`symbol`),
|
||||
INDEX `idx_module` (`module`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
|
||||
private Task CreateCoreBudgetAsync() => _db.ExecuteAsync(@"
|
||||
CREATE TABLE IF NOT EXISTS `core_budget` (
|
||||
`module` VARCHAR(50) NOT NULL PRIMARY KEY,
|
||||
`total_budget` DECIMAL(18,2) DEFAULT 0,
|
||||
`used_budget` DECIMAL(18,2) DEFAULT 0,
|
||||
`max_per_trade` DECIMAL(18,2) DEFAULT 0,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
|
||||
private Task CreateCorePositionAsync() => _db.ExecuteAsync(@"
|
||||
CREATE TABLE IF NOT EXISTS `core_position` (
|
||||
`module` VARCHAR(50) NOT NULL,
|
||||
`symbol` VARCHAR(20) NOT NULL,
|
||||
`quantity` INT NOT NULL DEFAULT 0,
|
||||
`avg_price` DECIMAL(18,4) NOT NULL DEFAULT 0,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`module`, `symbol`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
}
|
||||
@@ -14,8 +14,18 @@ public static class ServiceCollectionExtensions
|
||||
public static IServiceCollection AddCorePersistence(this IServiceCollection services, DatabaseOptions options)
|
||||
{
|
||||
services.AddDbContextFactory<CoreDbContext>(o =>
|
||||
o.UseMySql(options.MySqlConnectionString, DatabaseServerVersion.Value));
|
||||
o.UseMySql(EffectiveConnectionString(options.MySqlConnectionString), DatabaseServerVersion.Value));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liefert den Connection-String oder – bei leerem Wert (z. B. fehlende appsettings.Local.json) –
|
||||
/// einen unschädlichen Platzhalter, damit der DbContext-Optionsbau nicht wirft. Die App startet so
|
||||
/// unabhängig von der DB; echte Queries schlagen erst bei tatsächlichem Zugriff fehl.
|
||||
/// </summary>
|
||||
public static string EffectiveConnectionString(string? connectionString)
|
||||
=> string.IsNullOrWhiteSpace(connectionString)
|
||||
? "Server=localhost;Port=3306;Database=ibkrtrader;User ID=root;Password=;"
|
||||
: connectionString;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using IBKRTrader.Core.Persistence.Ef;
|
||||
using IBKRTrader.Core.Persistence.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace IBKRTrader.Core.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Key-Value-Zugriff auf core_settings (EF Core). Von Core und Modulen für Fortschritts-/Flag-Werte
|
||||
/// genutzt (z. B. "ct.history_import_page").
|
||||
/// </summary>
|
||||
public class CoreSettingsService
|
||||
{
|
||||
private readonly IDbContextFactory<CoreDbContext> _dbf;
|
||||
|
||||
public CoreSettingsService(IDbContextFactory<CoreDbContext> dbf) => _dbf = dbf;
|
||||
|
||||
public async Task<string?> GetAsync(string key)
|
||||
{
|
||||
await using var db = await _dbf.CreateDbContextAsync();
|
||||
var s = await db.Settings.FindAsync(key);
|
||||
return s?.Value;
|
||||
}
|
||||
|
||||
public async Task SetAsync(string key, string value)
|
||||
{
|
||||
await using var db = await _dbf.CreateDbContextAsync();
|
||||
var s = await db.Settings.FindAsync(key);
|
||||
if (s is null)
|
||||
db.Settings.Add(new CoreSetting { Key = key, Value = value, UpdatedAt = DateTime.UtcNow });
|
||||
else
|
||||
{
|
||||
s.Value = value;
|
||||
s.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string key)
|
||||
{
|
||||
await using var db = await _dbf.CreateDbContextAsync();
|
||||
var s = await db.Settings.FindAsync(key);
|
||||
if (s is not null)
|
||||
{
|
||||
db.Settings.Remove(s);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user