using Microsoft.EntityFrameworkCore; using Npgsql; using YarpGateway.Config; using YarpGateway.Models; namespace YarpGateway.Data; public class GatewayDbContext : DbContext { public GatewayDbContext(DbContextOptions options) : base(options) { } public DbSet Tenants => Set(); public DbSet TenantRoutes => Set(); public DbSet ServiceInstances => Set(); public override int SaveChanges(bool acceptAllChangesOnSuccess) { DetectConfigChanges(); var result = base.SaveChanges(acceptAllChangesOnSuccess); if (_configChangeDetected) { NotifyConfigChangedSync(); } return result; } public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { DetectConfigChanges(); var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); if (_configChangeDetected) { await NotifyConfigChangedAsync(cancellationToken); } return result; } private bool _configChangeDetected; private void DetectConfigChanges() { var entries = ChangeTracker.Entries() .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted) .Where(e => e.Entity is GwTenantRoute or GwServiceInstance or GwTenant); _configChangeDetected = entries.Any(); } private bool IsRelationalDatabase() { try { return Database.IsRelational(); } catch { return false; } } private void NotifyConfigChangedSync() { if (!IsRelationalDatabase()) return; var connectionString = Database.GetConnectionString(); if (string.IsNullOrEmpty(connectionString)) return; using var connection = new NpgsqlConnection(connectionString); connection.Open(); using var cmd = new NpgsqlCommand($"NOTIFY {ConfigNotifyChannel.GatewayConfigChanged}", connection); cmd.ExecuteNonQuery(); } private async Task NotifyConfigChangedAsync(CancellationToken cancellationToken) { if (!IsRelationalDatabase()) return; var connectionString = Database.GetConnectionString(); if (string.IsNullOrEmpty(connectionString)) return; await using var connection = new NpgsqlConnection(connectionString); await connection.OpenAsync(cancellationToken); await using var cmd = new NpgsqlCommand($"NOTIFY {ConfigNotifyChannel.GatewayConfigChanged}", connection); await cmd.ExecuteNonQueryAsync(cancellationToken); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.TenantCode).HasMaxLength(50).IsRequired(); entity.Property(e => e.TenantName).HasMaxLength(100).IsRequired(); entity.HasIndex(e => e.TenantCode).IsUnique(); }); modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.TenantCode).HasMaxLength(50); entity.Property(e => e.ServiceName).HasMaxLength(100).IsRequired(); entity.Property(e => e.ClusterId).HasMaxLength(100).IsRequired(); entity.Property(e => e.PathPattern).HasMaxLength(200).IsRequired(); entity.HasIndex(e => e.TenantCode); entity.HasIndex(e => e.ServiceName); entity.HasIndex(e => e.ClusterId); entity.HasIndex(e => new { e.ServiceName, e.IsGlobal, e.Status }); }); modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.ClusterId).HasMaxLength(100).IsRequired(); entity.Property(e => e.DestinationId).HasMaxLength(100).IsRequired(); entity.Property(e => e.Address).HasMaxLength(200).IsRequired(); entity.HasIndex(e => new { e.ClusterId, e.DestinationId }).IsUnique(); entity.HasIndex(e => e.Health); }); base.OnModelCreating(modelBuilder); } }