using Managing.Infrastructure.Databases.PostgreSql.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Managing.Infrastructure.Databases.PostgreSql; public class ManagingDbContext : DbContext { private readonly ILogger? _logger; private readonly SentrySqlMonitoringService? _sentryMonitoringService; private readonly Dictionary _queryExecutionCounts = new(); private readonly object _queryCountLock = new object(); public ManagingDbContext(DbContextOptions options) : base(options) { } public ManagingDbContext(DbContextOptions options, ILogger logger, SentrySqlMonitoringService sentryMonitoringService) : base(options) { _logger = logger; _sentryMonitoringService = sentryMonitoringService; } public DbSet Accounts { get; set; } public DbSet Users { get; set; } public DbSet GeneticRequests { get; set; } public DbSet Backtests { get; set; } public DbSet BundleBacktestRequests { get; set; } // Trading entities public DbSet Scenarios { get; set; } public DbSet Indicators { get; set; } public DbSet ScenarioIndicators { get; set; } public DbSet Signals { get; set; } public DbSet Positions { get; set; } public DbSet Trades { get; set; } // Statistics entities public DbSet TopVolumeTickers { get; set; } public DbSet SpotlightOverviews { get; set; } public DbSet Traders { get; set; } public DbSet FundingRates { get; set; } public DbSet AgentSummaries { get; set; } // Bot entities public DbSet Bots { get; set; } // Settings entities public DbSet MoneyManagements { get; set; } // Worker entities public DbSet Workers { get; set; } public DbSet SynthMinersLeaderboards { get; set; } public DbSet SynthPredictions { get; set; } public DbSet WhitelistAccounts { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // Configure schema for Orleans tables (if needed for future organization) // Orleans tables will remain in the default schema for now // This can be changed later if needed by configuring specific schemas // Configure Account entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.Name).IsRequired().HasMaxLength(255); entity.Property(e => e.Key).HasMaxLength(500); entity.Property(e => e.Secret).HasMaxLength(500); entity.Property(e => e.Exchange) .IsRequired() .HasConversion(); // Store enum as string entity.Property(e => e.Type) .IsRequired() .HasConversion(); // Store enum as string entity.Property(e => e.IsGmxInitialized) .IsRequired() .HasDefaultValue(false); // Default value for new records // Create unique index on account name entity.HasIndex(e => e.Name).IsUnique(); // Configure relationship with User entity.HasOne(e => e.User) .WithMany(u => u.Accounts) .HasForeignKey(e => e.UserId) .OnDelete(DeleteBehavior.SetNull); }); // Configure User entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.Name).IsRequired().HasMaxLength(255); entity.Property(e => e.AgentName).HasMaxLength(255); entity.Property(e => e.AvatarUrl).HasMaxLength(500); entity.Property(e => e.TelegramChannel).HasMaxLength(255); // Create indexes for performance entity.HasIndex(e => e.Name).IsUnique(); entity.HasIndex(e => e.AgentName); }); // Configure GeneticRequest entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.RequestId).IsRequired().HasMaxLength(255); entity.Property(e => e.Status).IsRequired().HasMaxLength(50); entity.Property(e => e.Ticker) .IsRequired() .HasConversion(); // Store enum as string entity.Property(e => e.Timeframe) .IsRequired() .HasConversion(); // Store enum as string entity.Property(e => e.SelectionMethod) .IsRequired() .HasConversion(); // Store enum as string entity.Property(e => e.CrossoverMethod) .IsRequired() .HasConversion(); // Store enum as string entity.Property(e => e.MutationMethod) .IsRequired() .HasConversion(); // Store enum as string entity.Property(e => e.Balance).HasColumnType("decimal(18,8)"); entity.Property(e => e.BestIndividual).HasMaxLength(4000); entity.Property(e => e.ErrorMessage).HasMaxLength(2000); entity.Property(e => e.ProgressInfo).HasMaxLength(4000); entity.Property(e => e.BestChromosome).HasMaxLength(4000); entity.Property(e => e.EligibleIndicatorsJson).HasMaxLength(2000); // Create indexes entity.HasIndex(e => e.RequestId).IsUnique(); entity.HasIndex(e => e.Status); // Configure relationship with User entity.HasOne(e => e.User) .WithMany() .HasForeignKey(e => e.UserId) .OnDelete(DeleteBehavior.SetNull); }); // Configure Backtest entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.Identifier).IsRequired().HasMaxLength(255); entity.Property(e => e.RequestId).IsRequired().HasMaxLength(255); entity.Property(e => e.UserId).IsRequired(); entity.Property(e => e.FinalPnl).HasColumnType("decimal(18,8)"); entity.Property(e => e.GrowthPercentage).HasColumnType("decimal(18,8)"); entity.Property(e => e.HodlPercentage).HasColumnType("decimal(18,8)"); entity.Property(e => e.Fees).HasColumnType("decimal(18,8)"); entity.Property(e => e.ConfigJson).HasColumnType("jsonb"); entity.Property(e => e.Name).IsRequired().HasMaxLength(255); entity.Property(e => e.Ticker).HasMaxLength(32); entity.Property(e => e.Timeframe).IsRequired(); entity.Property(e => e.IndicatorsCsv).HasColumnType("text"); entity.Property(e => e.IndicatorsCount).IsRequired(); entity.Property(e => e.PositionsJson).HasColumnType("jsonb"); entity.Property(e => e.SignalsJson).HasColumnType("jsonb"); entity.Property(e => e.MoneyManagementJson).HasColumnType("jsonb"); entity.Property(e => e.StatisticsJson).HasColumnType("jsonb"); entity.Property(e => e.SharpeRatio).HasColumnType("decimal(18,8)").HasDefaultValue(0m); entity.Property(e => e.MaxDrawdown).HasColumnType("decimal(18,8)").HasDefaultValue(0m); entity.Property(e => e.MaxDrawdownRecoveryTime).HasDefaultValue(TimeSpan.Zero); entity.Property(e => e.Duration).HasDefaultValue(TimeSpan.Zero); entity.Property(e => e.ScoreMessage).HasMaxLength(1000); entity.Property(e => e.Metadata).HasColumnType("text"); // Configure relationship with User entity.HasOne(e => e.User) .WithMany() .HasForeignKey(e => e.UserId) .OnDelete(DeleteBehavior.SetNull); // Create indexes for common queries entity.HasIndex(e => e.Identifier).IsUnique(); entity.HasIndex(e => e.RequestId); entity.HasIndex(e => e.UserId); entity.HasIndex(e => e.Score); // Composite indexes for efficient pagination and filtering entity.HasIndex(e => new { e.UserId, e.Score }); entity.HasIndex(e => new { e.UserId, e.Name }); entity.HasIndex(e => new { e.RequestId, e.Score }); entity.HasIndex(e => new { e.UserId, e.Ticker }); entity.HasIndex(e => new { e.UserId, e.Timeframe }); }); // Configure BundleBacktestRequest entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.RequestId).IsRequired().HasMaxLength(255); entity.Property(e => e.UserId); entity.Property(e => e.Name).IsRequired().HasMaxLength(255); entity.Property(e => e.Version).IsRequired().HasDefaultValue(1); entity.Property(e => e.Status) .IsRequired() .HasConversion(); // Store enum as string entity.Property(e => e.UniversalConfigJson).HasColumnType("text"); entity.Property(e => e.DateTimeRangesJson).HasColumnType("text"); entity.Property(e => e.MoneyManagementVariantsJson).HasColumnType("text"); entity.Property(e => e.TickerVariantsJson).HasColumnType("text"); entity.Property(e => e.ErrorMessage).HasColumnType("text"); entity.Property(e => e.ProgressInfo).HasColumnType("text"); entity.Property(e => e.CurrentBacktest).HasMaxLength(500); entity.Property(e => e.ResultsJson).HasColumnType("jsonb"); // Configure relationship with User entity.HasOne(e => e.User) .WithMany() .HasForeignKey(e => e.UserId) .OnDelete(DeleteBehavior.SetNull); // Create indexes for common queries entity.HasIndex(e => e.RequestId).IsUnique(); entity.HasIndex(e => e.UserId); entity.HasIndex(e => e.Status); // Composite index for user queries ordered by creation date entity.HasIndex(e => new { e.UserId, e.CreatedAt }); // Composite index for user queries by name and version entity.HasIndex(e => new { e.UserId, e.Name, e.Version }); }); // Configure Scenario entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.Name).IsRequired().HasMaxLength(255); entity.Property(e => e.UserId).IsRequired(); // Configure relationship with User entity.HasOne(e => e.User) .WithMany() .HasForeignKey(e => e.UserId) .OnDelete(DeleteBehavior.SetNull); // Create indexes entity.HasIndex(e => e.UserId); // Composite index for user scenarios entity.HasIndex(e => new { e.UserId, e.Name }); }); // Configure Indicator entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.Name).IsRequired().HasMaxLength(255); entity.Property(e => e.Type).IsRequired().HasConversion(); entity.Property(e => e.Timeframe).IsRequired().HasConversion(); entity.Property(e => e.SignalType).IsRequired().HasConversion(); entity.Property(e => e.UserId); // Configure relationship with User entity.HasOne(e => e.User) .WithMany() .HasForeignKey(e => e.UserId) .OnDelete(DeleteBehavior.SetNull); // Create indexes entity.HasIndex(e => e.UserId); // Composite index for user indicators entity.HasIndex(e => new { e.UserId, e.Name }); }); // Configure ScenarioIndicator junction table modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); // Configure relationships entity.HasOne(e => e.Scenario) .WithMany(s => s.ScenarioIndicators) .HasForeignKey(e => e.ScenarioId) .OnDelete(DeleteBehavior.Cascade); entity.HasOne(e => e.Indicator) .WithMany(i => i.ScenarioIndicators) .HasForeignKey(e => e.IndicatorId) .OnDelete(DeleteBehavior.Cascade); // Create indexes entity.HasIndex(e => new { e.ScenarioId, e.IndicatorId }).IsUnique(); }); // Configure Signal entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.Identifier).IsRequired().HasMaxLength(255); entity.Property(e => e.Direction).IsRequired().HasConversion(); entity.Property(e => e.Confidence).IsRequired().HasConversion(); entity.Property(e => e.Ticker).IsRequired().HasConversion(); entity.Property(e => e.Status).IsRequired().HasConversion(); entity.Property(e => e.Timeframe).IsRequired().HasConversion(); entity.Property(e => e.Type).IsRequired().HasConversion(); entity.Property(e => e.SignalType).IsRequired().HasConversion(); entity.Property(e => e.IndicatorName).IsRequired().HasMaxLength(255); entity.Property(e => e.UserId); entity.Property(e => e.CandleJson).HasColumnType("text"); // Configure relationship with User entity.HasOne(e => e.User) .WithMany() .HasForeignKey(e => e.UserId) .OnDelete(DeleteBehavior.SetNull); // Create indexes entity.HasIndex(e => e.Identifier); entity.HasIndex(e => e.UserId); entity.HasIndex(e => e.Date); entity.HasIndex(e => e.Ticker); entity.HasIndex(e => e.Status); // Composite indexes for common queries entity.HasIndex(e => new { e.UserId, e.Date }); entity.HasIndex(e => new { e.Identifier, e.Date, e.UserId }).IsUnique(); }); // Configure Position entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Identifier); entity.Property(e => e.ProfitAndLoss).HasColumnType("decimal(18,8)"); entity.Property(e => e.NetPnL).HasColumnType("decimal(18,8)"); entity.Property(e => e.OriginDirection).IsRequired().HasConversion(); entity.Property(e => e.Status).IsRequired().HasConversion(); entity.Property(e => e.Ticker).IsRequired().HasConversion(); entity.Property(e => e.Initiator).IsRequired().HasConversion(); entity.Property(e => e.SignalIdentifier).IsRequired().HasMaxLength(255); entity.Property(e => e.UserId); entity.Property(e => e.InitiatorIdentifier).IsRequired(); entity.Property(e => e.MoneyManagementJson).HasColumnType("text"); // Configure relationship with User entity.HasOne(e => e.User) .WithMany() .HasForeignKey(e => e.UserId) .OnDelete(DeleteBehavior.SetNull); // Configure relationships with trades entity.HasOne(e => e.OpenTrade) .WithMany() .HasForeignKey(e => e.OpenTradeId) .OnDelete(DeleteBehavior.SetNull); entity.HasOne(e => e.StopLossTrade) .WithMany() .HasForeignKey(e => e.StopLossTradeId) .OnDelete(DeleteBehavior.SetNull); entity.HasOne(e => e.TakeProfit1Trade) .WithMany() .HasForeignKey(e => e.TakeProfit1TradeId) .OnDelete(DeleteBehavior.SetNull); entity.HasOne(e => e.TakeProfit2Trade) .WithMany() .HasForeignKey(e => e.TakeProfit2TradeId) .OnDelete(DeleteBehavior.SetNull); // Create indexes entity.HasIndex(e => e.Identifier).IsUnique(); entity.HasIndex(e => e.UserId); entity.HasIndex(e => e.Status); entity.HasIndex(e => e.InitiatorIdentifier); // Composite indexes entity.HasIndex(e => new { e.UserId, e.Identifier }); }); // Configure Trade entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.Direction).IsRequired().HasConversion(); entity.Property(e => e.Status).IsRequired().HasConversion(); entity.Property(e => e.TradeType).IsRequired().HasConversion(); entity.Property(e => e.Ticker).IsRequired().HasConversion(); entity.Property(e => e.Quantity).HasColumnType("decimal(18,8)"); entity.Property(e => e.Price).HasColumnType("decimal(18,8)"); entity.Property(e => e.Leverage).HasColumnType("decimal(18,8)"); entity.Property(e => e.ExchangeOrderId).HasMaxLength(255); entity.Property(e => e.Message).HasColumnType("text"); // Create indexes entity.HasIndex(e => e.Date); entity.HasIndex(e => e.Status); entity.HasIndex(e => e.ExchangeOrderId); }); // Configure TopVolumeTicker entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.Volume).HasPrecision(18, 8); // Create indexes entity.HasIndex(e => e.Ticker); entity.HasIndex(e => e.Date); entity.HasIndex(e => e.Exchange); // Composite indexes for efficient queries entity.HasIndex(e => new { e.Exchange, e.Date }); entity.HasIndex(e => new { e.Date, e.Rank }); }); // Configure SpotlightOverview entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.Identifier).IsRequired(); entity.Property(e => e.SpotlightsJson).HasColumnType("jsonb"); // Create indexes entity.HasIndex(e => e.Identifier).IsUnique(); entity.HasIndex(e => e.DateTime); // Composite index for efficient queries entity.HasIndex(e => new { e.DateTime, e.ScenarioCount }); }); // Configure Trader entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.Address).IsRequired().HasMaxLength(255); entity.Property(e => e.Pnl).HasPrecision(18, 8); entity.Property(e => e.AverageWin).HasPrecision(18, 8); entity.Property(e => e.AverageLoss).HasPrecision(18, 8); entity.Property(e => e.Roi).HasPrecision(18, 8); // Create indexes entity.HasIndex(e => e.Address); entity.HasIndex(e => e.IsBestTrader); entity.HasIndex(e => e.Winrate); entity.HasIndex(e => e.Pnl); entity.HasIndex(e => e.Roi); // Composite indexes for efficient queries entity.HasIndex(e => new { e.IsBestTrader, e.Winrate }); entity.HasIndex(e => new { e.IsBestTrader, e.Roi }); entity.HasIndex(e => new { e.Address, e.IsBestTrader }).IsUnique(); }); // Configure FundingRate entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.Rate).HasPrecision(18, 8); entity.Property(e => e.OpenInterest).HasPrecision(18, 8); // Create indexes entity.HasIndex(e => e.Ticker); entity.HasIndex(e => e.Exchange); entity.HasIndex(e => e.Date); // Composite indexes for efficient queries entity.HasIndex(e => new { e.Ticker, e.Exchange }); entity.HasIndex(e => new { e.Exchange, e.Date }); entity.HasIndex(e => new { e.Ticker, e.Exchange, e.Date }).IsUnique(); }); // Configure BotBackup entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Identifier); entity.Property(e => e.Identifier).IsRequired().HasMaxLength(255); entity.Property(e => e.Name).IsRequired().HasMaxLength(255); entity.Property(e => e.Status).IsRequired().HasConversion(); entity.Property(e => e.CreateDate).IsRequired(); entity.Property(e => e.StartupTime).IsRequired(); // Runtime tracking fields entity.Property(e => e.LastStartTime); entity.Property(e => e.LastStopTime); entity.Property(e => e.AccumulatedRunTimeSeconds); entity.Property(e => e.TradeWins).IsRequired(); entity.Property(e => e.TradeLosses).IsRequired(); entity.Property(e => e.Pnl).HasPrecision(18, 8); entity.Property(e => e.NetPnL).HasPrecision(18, 8); entity.Property(e => e.Roi).HasPrecision(18, 8); entity.Property(e => e.Volume).HasPrecision(18, 8); entity.Property(e => e.Fees).HasPrecision(18, 8); entity.Property(e => e.LongPositionCount).IsRequired(); entity.Property(e => e.ShortPositionCount).IsRequired(); // Create indexes entity.HasIndex(e => e.Identifier).IsUnique(); entity.HasIndex(e => e.Status); // Configure relationship with User entity.HasOne(e => e.User) .WithMany() .HasForeignKey(e => e.UserId) .OnDelete(DeleteBehavior.SetNull); }); // Configure MoneyManagement entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.Name).IsRequired().HasMaxLength(255); entity.Property(e => e.Timeframe).IsRequired().HasConversion(); entity.Property(e => e.StopLoss).HasColumnType("decimal(18,8)"); entity.Property(e => e.TakeProfit).HasColumnType("decimal(18,8)"); entity.Property(e => e.Leverage).HasColumnType("decimal(18,8)"); entity.Property(e => e.UserName).HasMaxLength(255); // Create indexes entity.HasIndex(e => e.UserName); // Composite index for user money managements entity.HasIndex(e => new { e.UserName, e.Name }); // Configure relationship with User entity.HasOne(e => e.User) .WithMany() .HasForeignKey(e => e.UserId) .OnDelete(DeleteBehavior.SetNull); }); // Configure Worker entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.WorkerType).IsRequired().HasConversion(); entity.Property(e => e.StartTime).IsRequired(); entity.Property(e => e.LastRunTime); entity.Property(e => e.ExecutionCount).IsRequired(); entity.Property(e => e.DelayTicks).IsRequired(); entity.Property(e => e.IsActive).IsRequired(); entity.HasIndex(e => e.WorkerType).IsUnique(); }); // Configure SynthMinersLeaderboard entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.Asset).IsRequired().HasMaxLength(32); entity.Property(e => e.TimeIncrement).IsRequired(); entity.Property(e => e.SignalDate); entity.Property(e => e.IsBacktest).IsRequired(); entity.Property(e => e.MinersData).HasColumnType("jsonb"); entity.Property(e => e.CacheKey).IsRequired().HasMaxLength(255); entity.Property(e => e.CreatedAt).IsRequired(); entity.HasIndex(e => e.CacheKey).IsUnique(); }); // Configure SynthPrediction entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.Asset).IsRequired().HasMaxLength(32); entity.Property(e => e.MinerUid).IsRequired(); entity.Property(e => e.TimeIncrement).IsRequired(); entity.Property(e => e.TimeLength).IsRequired(); entity.Property(e => e.SignalDate); entity.Property(e => e.IsBacktest).IsRequired(); entity.Property(e => e.PredictionData).HasColumnType("jsonb"); entity.Property(e => e.CacheKey).IsRequired().HasMaxLength(255); entity.Property(e => e.CreatedAt).IsRequired(); entity.HasIndex(e => e.CacheKey).IsUnique(); }); // Configure WhitelistAccount entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.PrivyId).IsRequired().HasMaxLength(255); entity.Property(e => e.PrivyCreationDate).IsRequired(); entity.Property(e => e.EmbeddedWallet).IsRequired().HasMaxLength(42); entity.Property(e => e.ExternalEthereumAccount).HasMaxLength(42); entity.Property(e => e.TwitterAccount).HasMaxLength(255); entity.Property(e => e.IsWhitelisted) .IsRequired() .HasDefaultValue(false); entity.Property(e => e.CreatedAt).IsRequired(); entity.Property(e => e.UpdatedAt); // Create indexes for search performance entity.HasIndex(e => e.PrivyId).IsUnique(); entity.HasIndex(e => e.EmbeddedWallet).IsUnique(); entity.HasIndex(e => e.ExternalEthereumAccount); entity.HasIndex(e => e.TwitterAccount); entity.HasIndex(e => e.CreatedAt); }); // Configure AgentSummary entity modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.UserId).IsRequired(); entity.Property(e => e.AgentName).IsRequired().HasMaxLength(255); entity.Property(e => e.TotalPnL).HasColumnType("decimal(18,8)"); entity.Property(e => e.TotalROI).HasColumnType("decimal(18,8)"); entity.Property(e => e.Wins).IsRequired(); entity.Property(e => e.Losses).IsRequired(); entity.Property(e => e.Runtime); entity.Property(e => e.CreatedAt).IsRequired(); entity.Property(e => e.UpdatedAt).IsRequired(); entity.Property(e => e.ActiveStrategiesCount).IsRequired(); entity.Property(e => e.TotalVolume).HasPrecision(18, 8); entity.Property(e => e.TotalBalance).HasPrecision(18, 8); entity.Property(e => e.TotalFees).HasPrecision(18, 8); entity.Property(e => e.NetPnL).HasPrecision(18, 8); entity.Property(e => e.BacktestCount).IsRequired(); // Create indexes for common queries entity.HasIndex(e => e.UserId).IsUnique(); entity.HasIndex(e => e.AgentName).IsUnique(); entity.HasIndex(e => e.TotalPnL); // Configure relationship with User entity.HasOne(e => e.User) .WithMany() .HasForeignKey(e => e.UserId) .OnDelete(DeleteBehavior.Cascade); }); } /// /// Ensures Orleans tables are properly initialized in the database. /// This method can be called during application startup to verify Orleans infrastructure. /// public async Task EnsureOrleansTablesExistAsync() { // Orleans tables are automatically created by the Orleans framework // when using AdoNetClustering and AdoNetReminderService. // This method serves as a verification point and can be extended // for custom Orleans table management if needed. // For now, we just ensure the database is accessible await Database.CanConnectAsync(); } /// /// Gets Orleans table statistics for monitoring purposes. /// This helps track Orleans table sizes and performance. /// public async Task> GetOrleansTableStatsAsync() { var stats = new Dictionary(); // Orleans table names var orleansTables = new[] { "orleansmembershiptable", "orleansmembershipversiontable", "orleansquery", "orleansreminderstable", "orleansstorage" }; foreach (var tableName in orleansTables) { try { var count = await Database.SqlQueryRaw($"SELECT COUNT(*) FROM \"{tableName}\"").FirstOrDefaultAsync(); stats[tableName] = count; } catch { // Table might not exist yet (normal during startup) stats[tableName] = -1; } } return stats; } /// /// Database organization strategy: /// - Application tables: Default schema (public) /// - Orleans tables: Default schema (public) - managed by Orleans framework /// - Future consideration: Move Orleans tables to 'orleans' schema if needed /// /// Benefits of current approach: /// - Single database simplifies deployment and backup /// - Orleans tables are automatically managed by the framework /// - No additional configuration complexity /// - Easier monitoring and maintenance /// protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); // Add any additional configuration here if needed } /// /// Tracks query execution for loop detection and performance monitoring /// /// Pattern or hash of the query /// Time taken to execute the query /// Name of the repository executing the query /// Name of the method executing the query public void TrackQueryExecution(string queryPattern, TimeSpan executionTime, string repositoryName, string methodName) { if (_logger == null || _sentryMonitoringService == null) return; // Track execution count for this query pattern lock (_queryCountLock) { _queryExecutionCounts[queryPattern] = _queryExecutionCounts.GetValueOrDefault(queryPattern, 0) + 1; } // Check for potential loops with Sentry integration var isLoopDetected = _sentryMonitoringService.TrackQueryExecution(repositoryName, methodName, queryPattern, executionTime); // Log query execution details var logLevel = executionTime.TotalMilliseconds > 1000 ? LogLevel.Warning : LogLevel.Debug; _logger.Log(logLevel, "[SQL-QUERY-TRACKED] {Repository}.{Method} | Pattern: {Pattern} | Time: {Time}ms | Count: {Count}", repositoryName, methodName, queryPattern, executionTime.TotalMilliseconds, _queryExecutionCounts[queryPattern]); // Alert on potential loops if (isLoopDetected) { _logger.LogError( "[SQL-LOOP-ALERT] Potential infinite loop detected in {Repository}.{Method} with pattern '{Pattern}'", repositoryName, methodName, queryPattern); } } /// /// Gets current query execution statistics /// public Dictionary GetQueryExecutionCounts() { lock (_queryCountLock) { return new Dictionary(_queryExecutionCounts); } } /// /// Clears query execution tracking data /// public void ClearQueryTracking() { lock (_queryCountLock) { _queryExecutionCounts.Clear(); } _logger?.LogInformation("[SQL-TRACKING] Query execution counts cleared"); } }