From d8cfdbf6befed8a308b8ce2f4d47cb3a08a3e57b Mon Sep 17 00:00:00 2001 From: Richard Date: Sun, 5 Jul 2026 13:43:34 +0200 Subject: [PATCH] Phase 6 (Stufe 2): Core-EF-Repos + umschaltbare Registrierung - EF-Implementierungen hinter den Core-Interfaces: EfAccountRepository, EfMarketRepository, EfPositionRepository, EfTradeLogRepository. Thread-safe via IDbContextFactory (kurzlebiger Context je Operation). - AddCorePersistence(DatabaseOptions): Provider "MySql" -> EF + DbContextFactory, "Mongo" -> Mongo-Repos. Program.cs uebergibt die aufgeloesten Optionen. - appsettings.json Provider-Default = "Mongo" (App bleibt vorerst auf MongoDB, kein Verhaltenswechsel). MySQL-Connection liegt in gitignorierter appsettings.Local.json. - Build 0 Fehler. Naechste Stufen: Modul-Context + EF-Repos (inkl. trackers/mt_history), Config-Migration Mongo->MySQL, dann Provider umschalten + Mongo entfernen. Co-Authored-By: Claude Opus 4.8 --- Program.cs | 9 ++- appsettings.json | 1 + .../ServiceCollectionExtensions.cs | 34 ++++++-- .../Persistence/Ef/EfAccountRepository.cs | 42 ++++++++++ .../Persistence/Ef/EfMarketRepository.cs | 65 +++++++++++++++ .../Persistence/Ef/EfPositionRepository.cs | 80 +++++++++++++++++++ .../Persistence/Ef/EfTradeLogRepository.cs | 38 +++++++++ 7 files changed, 261 insertions(+), 8 deletions(-) create mode 100644 src/PolyTrader.Core/Persistence/Ef/EfAccountRepository.cs create mode 100644 src/PolyTrader.Core/Persistence/Ef/EfMarketRepository.cs create mode 100644 src/PolyTrader.Core/Persistence/Ef/EfPositionRepository.cs create mode 100644 src/PolyTrader.Core/Persistence/Ef/EfTradeLogRepository.cs diff --git a/Program.cs b/Program.cs index d642d4a..dd92633 100644 --- a/Program.cs +++ b/Program.cs @@ -34,6 +34,13 @@ internal static class Program AppHost = Host.CreateDefaultBuilder().ConfigureServices(delegate(HostBuilderContext context, IServiceCollection services) { + var databaseOptions = new DatabaseOptions + { + Provider = context.Configuration["Database:Provider"] ?? "Mongo", + MySqlConnectionString = context.Configuration["Database:MySqlConnectionString"] ?? string.Empty, + ConnectionString = context.Configuration["Database:ConnectionString"] ?? "mongodb://localhost:27017", + DatabaseName = context.Configuration["Database:DatabaseName"] ?? "PolyTraderDB" + }; services.Configure(context.Configuration.GetSection(DatabaseOptions.SectionName)); services.AddSingleton((IServiceProvider sp) => { @@ -41,7 +48,7 @@ internal static class Program var client = new MongoClient(dbOptions.ConnectionString); return client.GetDatabase(dbOptions.DatabaseName); }); - services.AddCorePersistence(); + services.AddCorePersistence(databaseOptions); services.AddSingleton(); services.AddSingleton((IServiceProvider sp) => ServerSettings.Load("server_settings.xml")); services.AddSingleton(); diff --git a/appsettings.json b/appsettings.json index 082f9e4..0de48a5 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,5 +1,6 @@ { "Database": { + "Provider": "Mongo", "ConnectionString": "mongodb://localhost:27017", "DatabaseName": "PolyTraderDB" } diff --git a/src/PolyTrader.Core/DependencyInjection/ServiceCollectionExtensions.cs b/src/PolyTrader.Core/DependencyInjection/ServiceCollectionExtensions.cs index 28c884b..12e1f3c 100644 --- a/src/PolyTrader.Core/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/PolyTrader.Core/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,5 +1,9 @@ +using System; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using PolyTrader.Core.Configuration; using PolyTrader.Core.Persistence; +using PolyTrader.Core.Persistence.Ef; using PolyTrader.Core.Persistence.Mongo; namespace PolyTrader.Core.DependencyInjection @@ -7,15 +11,31 @@ namespace PolyTrader.Core.DependencyInjection public static class ServiceCollectionExtensions { /// - /// Registriert die Core-Persistenzschicht (Repository-Interfaces + Mongo-Implementierungen). - /// Setzt eine registrierte IMongoDatabase voraus. + /// Registriert die Core-Persistenzschicht hinter den Repository-Interfaces. + /// Provider "MySql" -> EF Core (Pomelo) über einen DbContextFactory (thread-safe, + /// kurzlebiger Context je Operation). Provider "Mongo" -> Mongo-Implementierungen + /// (Übergang / Config-Migration). /// - public static IServiceCollection AddCorePersistence(this IServiceCollection services) + public static IServiceCollection AddCorePersistence(this IServiceCollection services, DatabaseOptions options) { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + if (string.Equals(options.Provider, "MySql", StringComparison.OrdinalIgnoreCase)) + { + var conn = options.MySqlConnectionString; + services.AddDbContextFactory(o => o.UseMySql(conn, ServerVersion.AutoDetect(conn))); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + else + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + return services; } } diff --git a/src/PolyTrader.Core/Persistence/Ef/EfAccountRepository.cs b/src/PolyTrader.Core/Persistence/Ef/EfAccountRepository.cs new file mode 100644 index 0000000..52fedfe --- /dev/null +++ b/src/PolyTrader.Core/Persistence/Ef/EfAccountRepository.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using PolyTraderSharp.Models; + +namespace PolyTrader.Core.Persistence.Ef +{ + public class EfAccountRepository : IAccountRepository + { + private readonly IDbContextFactory _factory; + + public EfAccountRepository(IDbContextFactory factory) => _factory = factory; + + public List GetAll() + { + using var ctx = _factory.CreateDbContext(); + return ctx.Accounts.AsNoTracking().ToList(); + } + + public void Upsert(AccountState account) + { + using var ctx = _factory.CreateDbContext(); + var existing = ctx.Accounts.Find(account.AccountId); + if (existing == null) + ctx.Accounts.Add(account); + else + ctx.Entry(existing).CurrentValues.SetValues(account); + ctx.SaveChanges(); + } + + public void Delete(int accountId) + { + using var ctx = _factory.CreateDbContext(); + var existing = ctx.Accounts.Find(accountId); + if (existing != null) + { + ctx.Accounts.Remove(existing); + ctx.SaveChanges(); + } + } + } +} diff --git a/src/PolyTrader.Core/Persistence/Ef/EfMarketRepository.cs b/src/PolyTrader.Core/Persistence/Ef/EfMarketRepository.cs new file mode 100644 index 0000000..99cfefc --- /dev/null +++ b/src/PolyTrader.Core/Persistence/Ef/EfMarketRepository.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using PolyTraderSharp.Models; + +namespace PolyTrader.Core.Persistence.Ef +{ + public class EfMarketRepository : IMarketRepository + { + private readonly IDbContextFactory _factory; + + public EfMarketRepository(IDbContextFactory factory) => _factory = factory; + + public MarketData? GetById(string id) + { + using var ctx = _factory.CreateDbContext(); + return ctx.Markets.AsNoTracking().FirstOrDefault(x => x.Id == id); + } + + public MarketData? FindByTokenId(string tokenId) + { + using var ctx = _factory.CreateDbContext(); + return ctx.Markets.AsNoTracking() + .FirstOrDefault(x => x.ClobTokenIds != null && x.ClobTokenIds.Contains(tokenId)); + } + + public List GetActive() + { + using var ctx = _factory.CreateDbContext(); + return ctx.Markets.AsNoTracking().Where(x => !x.Closed).ToList(); + } + + public void Upsert(MarketData market) + { + using var ctx = _factory.CreateDbContext(); + var existing = ctx.Markets.Find(market.Id); + if (existing == null) + ctx.Markets.Add(market); + else + ctx.Entry(existing).CurrentValues.SetValues(market); + ctx.SaveChanges(); + } + + public void Insert(MarketData market) + { + using var ctx = _factory.CreateDbContext(); + ctx.Markets.Add(market); + ctx.SaveChanges(); + } + + public void Update(MarketData market) + { + using var ctx = _factory.CreateDbContext(); + var existing = ctx.Markets.Find(market.Id); + if (existing != null) + { + ctx.Entry(existing).CurrentValues.SetValues(market); + ctx.SaveChanges(); + } + } + + // Indizes werden über die EF-Migration erstellt. + public void EnsureIndexes() { } + } +} diff --git a/src/PolyTrader.Core/Persistence/Ef/EfPositionRepository.cs b/src/PolyTrader.Core/Persistence/Ef/EfPositionRepository.cs new file mode 100644 index 0000000..26f8be4 --- /dev/null +++ b/src/PolyTrader.Core/Persistence/Ef/EfPositionRepository.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using PolyTraderSharp.Models; + +namespace PolyTrader.Core.Persistence.Ef +{ + public class EfPositionRepository : IPositionRepository + { + private readonly IDbContextFactory _factory; + + public EfPositionRepository(IDbContextFactory factory) => _factory = factory; + + public List GetLive(int accountId) + { + using var ctx = _factory.CreateDbContext(); + return ctx.Positions.AsNoTracking().Where(x => x.AccountId == accountId && !x.IsDemo).ToList(); + } + + public List GetDemo(int accountId) + { + using var ctx = _factory.CreateDbContext(); + return ctx.Positions.AsNoTracking().Where(x => x.AccountId == accountId && x.IsDemo).ToList(); + } + + public Position? FindLive(int accountId, string tokenId) + { + using var ctx = _factory.CreateDbContext(); + return ctx.Positions.AsNoTracking().FirstOrDefault(x => x.AccountId == accountId && !x.IsDemo && x.TokenId == tokenId); + } + + public Position? FindDemo(int accountId, string tokenId) + { + using var ctx = _factory.CreateDbContext(); + return ctx.Positions.AsNoTracking().FirstOrDefault(x => x.AccountId == accountId && x.IsDemo && x.TokenId == tokenId); + } + + public void UpsertLive(int accountId, Position position) => Upsert(accountId, false, position); + public void UpsertDemo(int accountId, Position position) => Upsert(accountId, true, position); + + private void Upsert(int accountId, bool isDemo, Position position) + { + position.AccountId = accountId; + position.IsDemo = isDemo; + + using var ctx = _factory.CreateDbContext(); + var existing = ctx.Positions.Find(accountId, isDemo, position.TokenId); + if (existing == null) + ctx.Positions.Add(position); + else + ctx.Entry(existing).CurrentValues.SetValues(position); + ctx.SaveChanges(); + } + + public void DeleteLive(int accountId, string tokenId) => Delete(accountId, false, tokenId); + public void DeleteDemo(int accountId, string tokenId) => Delete(accountId, true, tokenId); + + private void Delete(int accountId, bool isDemo, string tokenId) + { + using var ctx = _factory.CreateDbContext(); + var existing = ctx.Positions.Find(accountId, isDemo, tokenId); + if (existing != null) + { + ctx.Positions.Remove(existing); + ctx.SaveChanges(); + } + } + + public void DropDemo(int accountId) + { + using var ctx = _factory.CreateDbContext(); + var rows = ctx.Positions.Where(x => x.AccountId == accountId && x.IsDemo).ToList(); + if (rows.Count > 0) + { + ctx.Positions.RemoveRange(rows); + ctx.SaveChanges(); + } + } + } +} diff --git a/src/PolyTrader.Core/Persistence/Ef/EfTradeLogRepository.cs b/src/PolyTrader.Core/Persistence/Ef/EfTradeLogRepository.cs new file mode 100644 index 0000000..a9a05e2 --- /dev/null +++ b/src/PolyTrader.Core/Persistence/Ef/EfTradeLogRepository.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; +using PolyTraderSharp.Models; + +namespace PolyTrader.Core.Persistence.Ef +{ + public class EfTradeLogRepository : ITradeLogRepository + { + private readonly IDbContextFactory _factory; + + public EfTradeLogRepository(IDbContextFactory factory) => _factory = factory; + + // Indizes werden über die EF-Migration erstellt. + public void EnsureIndexes() { } + + public void Insert(TradeRecord record) + { + using var ctx = _factory.CreateDbContext(); + ctx.TradeLog.Add(record); + ctx.SaveChanges(); + } + + public List GetRecent(int limit) + { + using var ctx = _factory.CreateDbContext(); + return ctx.TradeLog.AsNoTracking().OrderByDescending(x => x.ClosedAt).Take(limit).ToList(); + } + + public List Find(Expression> predicate) + { + using var ctx = _factory.CreateDbContext(); + return ctx.TradeLog.AsNoTracking().Where(predicate).ToList(); + } + } +}