Phase 6 (Stufe 2): Core-EF-Repos + umschaltbare Registrierung

- EF-Implementierungen hinter den Core-Interfaces: EfAccountRepository,
  EfMarketRepository, EfPositionRepository, EfTradeLogRepository. Thread-safe via
  IDbContextFactory<CoreDbContext> (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 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-05 13:43:34 +02:00
co-authored by Claude Opus 4.8
parent ad9bb684d4
commit d8cfdbf6be
7 changed files with 261 additions and 8 deletions
+8 -1
View File
@@ -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<DatabaseOptions>(context.Configuration.GetSection(DatabaseOptions.SectionName));
services.AddSingleton<IMongoDatabase>((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<IBlockchainWssClientFactory, AlchemyWssClientFactory>();
services.AddSingleton((IServiceProvider sp) => ServerSettings.Load("server_settings.xml"));
services.AddSingleton<TradingState>();
+1
View File
@@ -1,5 +1,6 @@
{
"Database": {
"Provider": "Mongo",
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "PolyTraderDB"
}
@@ -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
{
/// <summary>
/// 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).
/// </summary>
public static IServiceCollection AddCorePersistence(this IServiceCollection services)
public static IServiceCollection AddCorePersistence(this IServiceCollection services, DatabaseOptions options)
{
if (string.Equals(options.Provider, "MySql", StringComparison.OrdinalIgnoreCase))
{
var conn = options.MySqlConnectionString;
services.AddDbContextFactory<CoreDbContext>(o => o.UseMySql(conn, ServerVersion.AutoDetect(conn)));
services.AddSingleton<IAccountRepository, EfAccountRepository>();
services.AddSingleton<IMarketRepository, EfMarketRepository>();
services.AddSingleton<IPositionRepository, EfPositionRepository>();
services.AddSingleton<ITradeLogRepository, EfTradeLogRepository>();
}
else
{
services.AddSingleton<IAccountRepository, MongoAccountRepository>();
services.AddSingleton<IMarketRepository, MongoMarketRepository>();
services.AddSingleton<IPositionRepository, MongoPositionRepository>();
services.AddSingleton<ITradeLogRepository, MongoTradeLogRepository>();
}
return services;
}
}
@@ -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<CoreDbContext> _factory;
public EfAccountRepository(IDbContextFactory<CoreDbContext> factory) => _factory = factory;
public List<AccountState> 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();
}
}
}
}
@@ -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<CoreDbContext> _factory;
public EfMarketRepository(IDbContextFactory<CoreDbContext> 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<MarketData> 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() { }
}
}
@@ -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<CoreDbContext> _factory;
public EfPositionRepository(IDbContextFactory<CoreDbContext> factory) => _factory = factory;
public List<Position> GetLive(int accountId)
{
using var ctx = _factory.CreateDbContext();
return ctx.Positions.AsNoTracking().Where(x => x.AccountId == accountId && !x.IsDemo).ToList();
}
public List<Position> 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();
}
}
}
}
@@ -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<CoreDbContext> _factory;
public EfTradeLogRepository(IDbContextFactory<CoreDbContext> 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<TradeRecord> GetRecent(int limit)
{
using var ctx = _factory.CreateDbContext();
return ctx.TradeLog.AsNoTracking().OrderByDescending(x => x.ClosedAt).Take(limit).ToList();
}
public List<TradeRecord> Find(Expression<Func<TradeRecord, bool>> predicate)
{
using var ctx = _factory.CreateDbContext();
return ctx.TradeLog.AsNoTracking().Where(predicate).ToList();
}
}
}