Files
Predictalytics/src/Predictalytics.Infrastructure/DependencyInjection.cs
T

141 lines
6.8 KiB
C#

using Predictalytics.Application.Interfaces;
using Predictalytics.Application.Services;
using Predictalytics.Infrastructure.Services;
using Predictalytics.Domain.Interfaces;
using Predictalytics.Infrastructure.Data;
using Predictalytics.Infrastructure.Data.Repositories;
using Predictalytics.Infrastructure.Providers.Azuro;
using Predictalytics.Infrastructure.Providers.Limitless;
using Predictalytics.Infrastructure.Providers.Polymarket;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Predictalytics.Infrastructure;
public static class DependencyInjection
{
public static IServiceCollection AddPredictalytics(this IServiceCollection services, IConfiguration configuration, string? connectionStringOverride = null, bool dbDebug = false)
{
if (dbDebug) Serilog.Log.Warning(">>> INFRASTRUCTURE: AddPredictalytics STARTING");
// MySQL / EF Core
var connectionString = connectionStringOverride;
if (string.IsNullOrWhiteSpace(connectionString))
{
connectionString = configuration.GetConnectionString("DefaultConnection")
?? "Server=localhost;Database=Predictalytics_dev;User=root;Password=;";
}
// Use MySqlConnectionStringBuilder to ensure valid format and parse components
var csBuilder = new MySqlConnector.MySqlConnectionStringBuilder(connectionString);
// Final safety check
if (string.IsNullOrWhiteSpace(csBuilder.Database))
{
Serilog.Log.Error("❌ INVALID CONNECTION STRING: Database name is empty! (Input length: {Length})", connectionString.Length);
throw new InvalidOperationException("The connection string is missing a valid 'Database' parameter.");
}
var maskedCs = csBuilder.ConnectionString.Replace(csBuilder.Password, "****");
if (dbDebug)
{
Serilog.Log.Warning("🗄️ Initializing database connection: {ConnectionString}", maskedCs);
Serilog.Log.Warning("🗄️ Target Server: {Server}, Database: {Database}", csBuilder.Server, csBuilder.Database);
}
// Explicitly register the connection string so we can use it elsewhere if needed
services.AddSingleton(csBuilder.ConnectionString);
services.AddDbContext<AppDbContext>(options =>
{
// Log exactly what is being used at the moment of configuration
if (dbDebug) Serilog.Log.Warning("🛠️ EF: Configuring AppDbContext. Target DB: '{Database}'", csBuilder.Database);
options.UseMySql(csBuilder.ConnectionString, new MySqlServerVersion(new Version(8, 0, 31)),
mysql => mysql.EnableRetryOnFailure(3, TimeSpan.FromSeconds(10), null));
});
// Repositories
services.AddScoped<ITraderRepository, TraderRepository>();
services.AddScoped<ITradeRepository, TradeRepository>();
services.AddScoped<IMarketRepository, MarketRepository>();
services.AddScoped<IWatchlistRepository, WatchlistRepository>();
services.AddScoped<IAlertRepository, AlertRepository>();
// Application Services
services.AddScoped<IPositionPnLEngine, PositionPnLEngine>();
services.AddScoped<IScoringService, ScoringService>();
services.AddScoped<IDiscoveryService, DiscoveryService>();
services.AddScoped<IAlertService, AlertService>();
services.AddScoped<IAnalyticsService, AnalyticsService>();
services.AddScoped<WatchlistService>();
services.AddSingleton<IRateLimiter, RateLimiterService>();
services.AddSingleton<IPlatformStatisticsService, PlatformStatisticsService>();
// Platform Providers
services.AddHttpClient();
services.AddHttpClient("LimitlessApi", c =>
{
c.BaseAddress = new Uri("https://api.limitless.exchange/");
c.DefaultRequestHeaders.Add("Accept", "application/json");
c.Timeout = TimeSpan.FromSeconds(60);
});
services.AddSingleton<PolymarketApiClient>();
services.AddSingleton<LimitlessApiClient>();
services.AddHttpClient<Predictalytics.Application.Interfaces.IOpenRouterApiClient, Predictalytics.Infrastructure.Providers.OpenRouter.OpenRouterApiClient>();
services.AddScoped<Predictalytics.Application.Interfaces.IAiStrategyAnalysisService, Predictalytics.Application.Services.AiStrategyAnalysisService>();
services.AddSingleton<IPlatformProvider, PolymarketProvider>();
services.AddSingleton<IPlatformProvider, LimitlessProvider>();
services.AddSingleton<IPlatformProvider, AzuroProvider>();
return services;
}
/// <summary>
/// Applies pending EF Core migrations and seeds default platform rows.
/// Requires the target database to already be "stamped" with the InitialBaseline
/// migration in __EFMigrationsHistory (see UMSETZUNGSPLAN.md, section B1) if it was
/// previously created via the old EnsureCreated + manual ALTER approach.
/// </summary>
public static async Task EnsureDatabaseAsync(IServiceProvider services, bool dbDebug = false)
{
using var scope = services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
try
{
if (dbDebug)
{
var maskedConnStr = System.Text.RegularExpressions.Regex.Replace(
db.Database.GetDbConnection().ConnectionString ?? "NULL", "Password=[^;]+", "Password=****");
Serilog.Log.Warning("🔍 DEBUG: Applying EF Core migrations. ConnectionString: {CS}", maskedConnStr);
}
await db.Database.MigrateAsync();
if (dbDebug) Serilog.Log.Warning("✅ DEBUG: Migrations applied successfully.");
// Seed default platform rows (idempotent)
var conn = db.Database.GetDbConnection();
if (conn.State != System.Data.ConnectionState.Open) await conn.OpenAsync();
using var seedPlatform = conn.CreateCommand();
seedPlatform.CommandText = @"
INSERT IGNORE INTO `PlatformConfigs` (`Id`, `Name`, `DisplayName`, `IsActive`, `CreatedAt`, `UpdatedAt`) VALUES
(0, 'Unknown', 'Unknown Platform', 1, UTC_TIMESTAMP(), UTC_TIMESTAMP()),
(1, 'Polymarket', 'Polymarket', 1, UTC_TIMESTAMP(), UTC_TIMESTAMP()),
(2, 'Azuro', 'Azuro', 1, UTC_TIMESTAMP(), UTC_TIMESTAMP()),
(3, 'Limitless', 'Limitless', 1, UTC_TIMESTAMP(), UTC_TIMESTAMP());";
await seedPlatform.ExecuteNonQueryAsync();
await conn.CloseAsync();
}
catch (Exception ex)
{
Serilog.Log.Warning("⚠️ Could not connect to database or apply migrations: {Message}. Background workers will retry connection automatically.", ex.Message);
}
}
}