Changes: - Remove deprecated Fengling.Activity and YarpGateway.Admin projects - Add points processing services with distributed lock support - Update Vben frontend with gateway management pages - Add gateway config controller and database listener - Update routing to use header-mixed-nav layout - Add comprehensive test suites for Member services - Add YarpGateway integration tests - Update package versions in Directory.Packages.props Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
126 lines
4.3 KiB
C#
126 lines
4.3 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Npgsql;
|
|
using YarpGateway.Config;
|
|
using YarpGateway.Models;
|
|
|
|
namespace YarpGateway.Data;
|
|
|
|
public class GatewayDbContext : DbContext
|
|
{
|
|
public GatewayDbContext(DbContextOptions<GatewayDbContext> options)
|
|
: base(options)
|
|
{
|
|
}
|
|
|
|
public DbSet<GwTenant> Tenants => Set<GwTenant>();
|
|
public DbSet<GwTenantRoute> TenantRoutes => Set<GwTenantRoute>();
|
|
public DbSet<GwServiceInstance> ServiceInstances => Set<GwServiceInstance>();
|
|
|
|
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
|
{
|
|
DetectConfigChanges();
|
|
var result = base.SaveChanges(acceptAllChangesOnSuccess);
|
|
if (_configChangeDetected)
|
|
{
|
|
NotifyConfigChangedSync();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public override async Task<int> 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<GwTenant>(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<GwTenantRoute>(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<GwServiceInstance>(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);
|
|
}
|
|
}
|