chore(auth): upgrade OpenIddict to 7.2.0
This commit is contained in:
commit
1be6309567
99
Config/DatabaseClusterConfigProvider.cs
Normal file
99
Config/DatabaseClusterConfigProvider.cs
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
using Yarp.ReverseProxy.Configuration;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using YarpGateway.Data;
|
||||||
|
using YarpGateway.Models;
|
||||||
|
|
||||||
|
namespace YarpGateway.Config;
|
||||||
|
|
||||||
|
public class DatabaseClusterConfigProvider
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<GatewayDbContext> _dbContextFactory;
|
||||||
|
private readonly ConcurrentDictionary<string, ClusterConfig> _clusters = new();
|
||||||
|
private readonly SemaphoreSlim _lock = new(1, 1);
|
||||||
|
private readonly ILogger<DatabaseClusterConfigProvider> _logger;
|
||||||
|
|
||||||
|
public DatabaseClusterConfigProvider(IDbContextFactory<GatewayDbContext> dbContextFactory, ILogger<DatabaseClusterConfigProvider> logger)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
_logger = logger;
|
||||||
|
_ = LoadConfigAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<ClusterConfig> GetClusters()
|
||||||
|
{
|
||||||
|
return _clusters.Values.ToList().AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ReloadAsync()
|
||||||
|
{
|
||||||
|
await _lock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await LoadConfigInternalAsync();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_lock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadConfigAsync()
|
||||||
|
{
|
||||||
|
await LoadConfigInternalAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadConfigInternalAsync()
|
||||||
|
{
|
||||||
|
await using var dbContext = _dbContextFactory.CreateDbContext();
|
||||||
|
|
||||||
|
var instances = await dbContext.ServiceInstances
|
||||||
|
.Where(i => i.Status == 1 && !i.IsDeleted)
|
||||||
|
.GroupBy(i => i.ClusterId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var newClusters = new ConcurrentDictionary<string, ClusterConfig>();
|
||||||
|
|
||||||
|
foreach (var group in instances)
|
||||||
|
{
|
||||||
|
var destinations = new Dictionary<string, DestinationConfig>();
|
||||||
|
foreach (var instance in group)
|
||||||
|
{
|
||||||
|
destinations[instance.DestinationId] = new DestinationConfig
|
||||||
|
{
|
||||||
|
Address = instance.Address,
|
||||||
|
Metadata = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["Weight"] = instance.Weight.ToString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = new ClusterConfig
|
||||||
|
{
|
||||||
|
ClusterId = group.Key,
|
||||||
|
Destinations = destinations,
|
||||||
|
LoadBalancingPolicy = "DistributedWeightedRoundRobin",
|
||||||
|
HealthCheck = new HealthCheckConfig
|
||||||
|
{
|
||||||
|
Active = new ActiveHealthCheckConfig
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Interval = TimeSpan.FromSeconds(30),
|
||||||
|
Timeout = TimeSpan.FromSeconds(5),
|
||||||
|
Path = "/health"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
newClusters[group.Key] = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
_clusters.Clear();
|
||||||
|
foreach (var cluster in newClusters)
|
||||||
|
{
|
||||||
|
_clusters[cluster.Key] = cluster.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Loaded {Count} clusters from database", _clusters.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
83
Config/DatabaseRouteConfigProvider.cs
Normal file
83
Config/DatabaseRouteConfigProvider.cs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Yarp.ReverseProxy.Configuration;
|
||||||
|
using YarpGateway.Data;
|
||||||
|
using YarpGateway.Models;
|
||||||
|
|
||||||
|
namespace YarpGateway.Config;
|
||||||
|
|
||||||
|
public class DatabaseRouteConfigProvider
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<GatewayDbContext> _dbContextFactory;
|
||||||
|
private readonly ConcurrentDictionary<string, RouteConfig> _routes = new();
|
||||||
|
private readonly SemaphoreSlim _lock = new(1, 1);
|
||||||
|
private readonly ILogger<DatabaseRouteConfigProvider> _logger;
|
||||||
|
|
||||||
|
public DatabaseRouteConfigProvider(
|
||||||
|
IDbContextFactory<GatewayDbContext> dbContextFactory,
|
||||||
|
ILogger<DatabaseRouteConfigProvider> logger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
_logger = logger;
|
||||||
|
_ = LoadConfigAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<RouteConfig> GetRoutes()
|
||||||
|
{
|
||||||
|
return _routes.Values.ToList().AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ReloadAsync()
|
||||||
|
{
|
||||||
|
await _lock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await LoadConfigInternalAsync();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_lock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadConfigAsync()
|
||||||
|
{
|
||||||
|
await LoadConfigInternalAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadConfigInternalAsync()
|
||||||
|
{
|
||||||
|
await using var dbContext = _dbContextFactory.CreateDbContext();
|
||||||
|
|
||||||
|
var routes = await dbContext
|
||||||
|
.TenantRoutes.Where(r => r.Status == 1 && !r.IsDeleted)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var newRoutes = new ConcurrentDictionary<string, RouteConfig>();
|
||||||
|
|
||||||
|
foreach (var route in routes)
|
||||||
|
{
|
||||||
|
var config = new RouteConfig
|
||||||
|
{
|
||||||
|
RouteId = route.Id.ToString(),
|
||||||
|
ClusterId = route.ClusterId,
|
||||||
|
Match = new RouteMatch { Path = route.PathPattern },
|
||||||
|
Metadata = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["TenantCode"] = route.TenantCode,
|
||||||
|
["ServiceName"] = route.ServiceName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
newRoutes[route.Id.ToString()] = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
_routes.Clear();
|
||||||
|
foreach (var route in newRoutes)
|
||||||
|
{
|
||||||
|
_routes[route.Key] = route.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Loaded {Count} routes from database", _routes.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Config/JwtConfig.cs
Normal file
9
Config/JwtConfig.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace YarpGateway.Config;
|
||||||
|
|
||||||
|
public class JwtConfig
|
||||||
|
{
|
||||||
|
public string Authority { get; set; } = string.Empty;
|
||||||
|
public string Audience { get; set; } = string.Empty;
|
||||||
|
public bool ValidateIssuer { get; set; } = true;
|
||||||
|
public bool ValidateAudience { get; set; } = true;
|
||||||
|
}
|
||||||
8
Config/RedisConfig.cs
Normal file
8
Config/RedisConfig.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace YarpGateway.Config;
|
||||||
|
|
||||||
|
public class RedisConfig
|
||||||
|
{
|
||||||
|
public string ConnectionString { get; set; } = "localhost:6379";
|
||||||
|
public int Database { get; set; } = 0;
|
||||||
|
public string InstanceName { get; set; } = "YarpGateway";
|
||||||
|
}
|
||||||
272
Controllers/GatewayConfigController.cs
Normal file
272
Controllers/GatewayConfigController.cs
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using YarpGateway.Data;
|
||||||
|
using YarpGateway.Config;
|
||||||
|
using YarpGateway.Models;
|
||||||
|
using YarpGateway.Services;
|
||||||
|
using Yarp.ReverseProxy.Configuration;
|
||||||
|
|
||||||
|
namespace YarpGateway.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/gateway")]
|
||||||
|
public class GatewayConfigController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<GatewayDbContext> _dbContextFactory;
|
||||||
|
private readonly DatabaseRouteConfigProvider _routeProvider;
|
||||||
|
private readonly DatabaseClusterConfigProvider _clusterProvider;
|
||||||
|
private readonly IRouteCache _routeCache;
|
||||||
|
|
||||||
|
public GatewayConfigController(
|
||||||
|
IDbContextFactory<GatewayDbContext> dbContextFactory,
|
||||||
|
DatabaseRouteConfigProvider routeProvider,
|
||||||
|
DatabaseClusterConfigProvider clusterProvider,
|
||||||
|
IRouteCache routeCache)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
_routeProvider = routeProvider;
|
||||||
|
_clusterProvider = clusterProvider;
|
||||||
|
_routeCache = routeCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("tenants")]
|
||||||
|
public async Task<IActionResult> GetTenants()
|
||||||
|
{
|
||||||
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
|
var tenants = await db.Tenants
|
||||||
|
.Where(t => !t.IsDeleted)
|
||||||
|
.ToListAsync();
|
||||||
|
return Ok(tenants);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("tenants")]
|
||||||
|
public async Task<IActionResult> CreateTenant([FromBody] CreateTenantDto dto)
|
||||||
|
{
|
||||||
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
|
var existing = await db.Tenants
|
||||||
|
.FirstOrDefaultAsync(t => t.TenantCode == dto.TenantCode);
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
return BadRequest($"Tenant code {dto.TenantCode} already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
var tenant = new GwTenant
|
||||||
|
{
|
||||||
|
Id = GenerateId(),
|
||||||
|
TenantCode = dto.TenantCode,
|
||||||
|
TenantName = dto.TenantName,
|
||||||
|
Status = 1
|
||||||
|
};
|
||||||
|
await db.Tenants.AddAsync(tenant);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok(tenant);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("tenants/{id}")]
|
||||||
|
public async Task<IActionResult> DeleteTenant(long id)
|
||||||
|
{
|
||||||
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
|
var tenant = await db.Tenants.FindAsync(id);
|
||||||
|
if (tenant == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
tenant.IsDeleted = true;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("tenants/{tenantCode}/routes")]
|
||||||
|
public async Task<IActionResult> GetTenantRoutes(string tenantCode)
|
||||||
|
{
|
||||||
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
|
var routes = await db.TenantRoutes
|
||||||
|
.Where(r => r.TenantCode == tenantCode && !r.IsDeleted)
|
||||||
|
.ToListAsync();
|
||||||
|
return Ok(routes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("tenants/{tenantCode}/routes")]
|
||||||
|
public async Task<IActionResult> CreateTenantRoute(string tenantCode, [FromBody] CreateTenantRouteDto dto)
|
||||||
|
{
|
||||||
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
|
var tenant = await db.Tenants
|
||||||
|
.FirstOrDefaultAsync(t => t.TenantCode == tenantCode);
|
||||||
|
if (tenant == null)
|
||||||
|
return BadRequest($"Tenant {tenantCode} not found");
|
||||||
|
|
||||||
|
var clusterId = $"{tenantCode}-{dto.ServiceName}";
|
||||||
|
var existing = await db.TenantRoutes
|
||||||
|
.FirstOrDefaultAsync(r => r.ClusterId == clusterId);
|
||||||
|
if (existing != null)
|
||||||
|
return BadRequest($"Route for {tenantCode}/{dto.ServiceName} already exists");
|
||||||
|
|
||||||
|
var route = new GwTenantRoute
|
||||||
|
{
|
||||||
|
Id = GenerateId(),
|
||||||
|
TenantCode = tenantCode,
|
||||||
|
ServiceName = dto.ServiceName,
|
||||||
|
ClusterId = clusterId,
|
||||||
|
PathPattern = dto.PathPattern,
|
||||||
|
Priority = 10,
|
||||||
|
Status = 1,
|
||||||
|
IsGlobal = false
|
||||||
|
};
|
||||||
|
await db.TenantRoutes.AddAsync(route);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await _routeCache.ReloadAsync();
|
||||||
|
|
||||||
|
return Ok(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("routes/global")]
|
||||||
|
public async Task<IActionResult> GetGlobalRoutes()
|
||||||
|
{
|
||||||
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
|
var routes = await db.TenantRoutes
|
||||||
|
.Where(r => r.IsGlobal && !r.IsDeleted)
|
||||||
|
.ToListAsync();
|
||||||
|
return Ok(routes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("routes/global")]
|
||||||
|
public async Task<IActionResult> CreateGlobalRoute([FromBody] CreateGlobalRouteDto dto)
|
||||||
|
{
|
||||||
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
|
var existing = await db.TenantRoutes
|
||||||
|
.FirstOrDefaultAsync(r => r.ServiceName == dto.ServiceName && r.IsGlobal);
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
return BadRequest($"Global route for {dto.ServiceName} already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
var route = new GwTenantRoute
|
||||||
|
{
|
||||||
|
Id = GenerateId(),
|
||||||
|
TenantCode = string.Empty,
|
||||||
|
ServiceName = dto.ServiceName,
|
||||||
|
ClusterId = dto.ClusterId,
|
||||||
|
PathPattern = dto.PathPattern,
|
||||||
|
Priority = 0,
|
||||||
|
Status = 1,
|
||||||
|
IsGlobal = true
|
||||||
|
};
|
||||||
|
await db.TenantRoutes.AddAsync(route);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await _routeCache.ReloadAsync();
|
||||||
|
|
||||||
|
return Ok(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("routes/{id}")]
|
||||||
|
public async Task<IActionResult> DeleteRoute(long id)
|
||||||
|
{
|
||||||
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
|
var route = await db.TenantRoutes.FindAsync(id);
|
||||||
|
if (route == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
route.IsDeleted = true;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await _routeCache.ReloadAsync();
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("clusters/{clusterId}/instances")]
|
||||||
|
public async Task<IActionResult> GetInstances(string clusterId)
|
||||||
|
{
|
||||||
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
|
var instances = await db.ServiceInstances
|
||||||
|
.Where(i => i.ClusterId == clusterId && !i.IsDeleted)
|
||||||
|
.ToListAsync();
|
||||||
|
return Ok(instances);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("clusters/{clusterId}/instances")]
|
||||||
|
public async Task<IActionResult> AddInstance(string clusterId, [FromBody] CreateInstanceDto dto)
|
||||||
|
{
|
||||||
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
|
var existing = await db.ServiceInstances
|
||||||
|
.FirstOrDefaultAsync(i => i.ClusterId == clusterId && i.DestinationId == dto.DestinationId);
|
||||||
|
if (existing != null)
|
||||||
|
return BadRequest($"Instance {dto.DestinationId} already exists in cluster {clusterId}");
|
||||||
|
|
||||||
|
var instance = new GwServiceInstance
|
||||||
|
{
|
||||||
|
Id = GenerateId(),
|
||||||
|
ClusterId = clusterId,
|
||||||
|
DestinationId = dto.DestinationId,
|
||||||
|
Address = dto.Address,
|
||||||
|
Weight = dto.Weight,
|
||||||
|
Health = 1,
|
||||||
|
Status = 1
|
||||||
|
};
|
||||||
|
await db.ServiceInstances.AddAsync(instance);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await _clusterProvider.ReloadAsync();
|
||||||
|
|
||||||
|
return Ok(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("instances/{id}")]
|
||||||
|
public async Task<IActionResult> DeleteInstance(long id)
|
||||||
|
{
|
||||||
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
|
var instance = await db.ServiceInstances.FindAsync(id);
|
||||||
|
if (instance == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
instance.IsDeleted = true;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await _clusterProvider.ReloadAsync();
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("reload")]
|
||||||
|
public async Task<IActionResult> ReloadConfig()
|
||||||
|
{
|
||||||
|
await _routeCache.ReloadAsync();
|
||||||
|
await _routeProvider.ReloadAsync();
|
||||||
|
await _clusterProvider.ReloadAsync();
|
||||||
|
return Ok(new { message = "Config reloaded successfully" });
|
||||||
|
}
|
||||||
|
|
||||||
|
private long GenerateId()
|
||||||
|
{
|
||||||
|
return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreateTenantDto
|
||||||
|
{
|
||||||
|
public string TenantCode { get; set; } = string.Empty;
|
||||||
|
public string TenantName { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreateTenantRouteDto
|
||||||
|
{
|
||||||
|
public string ServiceName { get; set; } = string.Empty;
|
||||||
|
public string PathPattern { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreateGlobalRouteDto
|
||||||
|
{
|
||||||
|
public string ServiceName { get; set; } = string.Empty;
|
||||||
|
public string ClusterId { get; set; } = string.Empty;
|
||||||
|
public string PathPattern { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreateInstanceDto
|
||||||
|
{
|
||||||
|
public string DestinationId { get; set; } = string.Empty;
|
||||||
|
public string Address { get; set; } = string.Empty;
|
||||||
|
public int Weight { get; set; } = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
Data/GatewayDbContext.cs
Normal file
52
Data/GatewayDbContext.cs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
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>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Data/GatewayDbContextFactory.cs
Normal file
22
Data/GatewayDbContextFactory.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace YarpGateway.Data;
|
||||||
|
|
||||||
|
public class GatewayDbContextFactory : IDesignTimeDbContextFactory<GatewayDbContext>
|
||||||
|
{
|
||||||
|
public GatewayDbContext CreateDbContext(string[] args)
|
||||||
|
{
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.SetBasePath(Directory.GetCurrentDirectory())
|
||||||
|
.AddJsonFile("appsettings.json", optional: false)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var optionsBuilder = new DbContextOptionsBuilder<GatewayDbContext>();
|
||||||
|
var connectionString = configuration.GetConnectionString("DefaultConnection");
|
||||||
|
optionsBuilder.UseNpgsql(connectionString);
|
||||||
|
|
||||||
|
return new GatewayDbContext(optionsBuilder.Options);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
RUN dotnet restore
|
||||||
|
RUN dotnet publish -c Release -o /app/publish
|
||||||
|
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/publish .
|
||||||
|
ENTRYPOINT ["dotnet", "YarpGateway.dll"]
|
||||||
69
DynamicProxy/DynamicProxyConfigProvider.cs
Normal file
69
DynamicProxy/DynamicProxyConfigProvider.cs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Yarp.ReverseProxy.Configuration;
|
||||||
|
using YarpGateway.Config;
|
||||||
|
|
||||||
|
namespace YarpGateway.DynamicProxy;
|
||||||
|
|
||||||
|
public class DynamicProxyConfigProvider : IProxyConfigProvider
|
||||||
|
{
|
||||||
|
private volatile IProxyConfig _config;
|
||||||
|
private readonly DatabaseRouteConfigProvider _routeProvider;
|
||||||
|
private readonly DatabaseClusterConfigProvider _clusterProvider;
|
||||||
|
private readonly object _lock = new();
|
||||||
|
|
||||||
|
public DynamicProxyConfigProvider(
|
||||||
|
DatabaseRouteConfigProvider routeProvider,
|
||||||
|
DatabaseClusterConfigProvider clusterProvider)
|
||||||
|
{
|
||||||
|
_routeProvider = routeProvider;
|
||||||
|
_clusterProvider = clusterProvider;
|
||||||
|
UpdateConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IProxyConfig GetConfig()
|
||||||
|
{
|
||||||
|
return _config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateConfig()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var routes = _routeProvider.GetRoutes();
|
||||||
|
var clusters = _clusterProvider.GetClusters();
|
||||||
|
|
||||||
|
_config = new InMemoryProxyConfig(
|
||||||
|
routes,
|
||||||
|
clusters,
|
||||||
|
Array.Empty<IReadOnlyDictionary<string, string>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ReloadAsync()
|
||||||
|
{
|
||||||
|
await _routeProvider.ReloadAsync();
|
||||||
|
await _clusterProvider.ReloadAsync();
|
||||||
|
UpdateConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class InMemoryProxyConfig : IProxyConfig
|
||||||
|
{
|
||||||
|
private static readonly CancellationChangeToken _nullChangeToken = new(new CancellationToken());
|
||||||
|
|
||||||
|
public InMemoryProxyConfig(
|
||||||
|
IReadOnlyList<RouteConfig> routes,
|
||||||
|
IReadOnlyList<ClusterConfig> clusters,
|
||||||
|
IReadOnlyList<IReadOnlyDictionary<string, string>> transforms)
|
||||||
|
{
|
||||||
|
Routes = routes;
|
||||||
|
Clusters = clusters;
|
||||||
|
Transforms = transforms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<RouteConfig> Routes { get; }
|
||||||
|
public IReadOnlyList<ClusterConfig> Clusters { get; }
|
||||||
|
public IReadOnlyList<IReadOnlyDictionary<string, string>> Transforms { get; }
|
||||||
|
public IChangeToken ChangeToken => _nullChangeToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
243
LoadBalancing/DistributedWeightedRoundRobinPolicy.cs
Normal file
243
LoadBalancing/DistributedWeightedRoundRobinPolicy.cs
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using Yarp.ReverseProxy.LoadBalancing;
|
||||||
|
using Yarp.ReverseProxy.Model;
|
||||||
|
using YarpGateway.Config;
|
||||||
|
|
||||||
|
namespace YarpGateway.LoadBalancing;
|
||||||
|
|
||||||
|
public class DistributedWeightedRoundRobinPolicy : ILoadBalancingPolicy
|
||||||
|
{
|
||||||
|
private readonly IConnectionMultiplexer _redis;
|
||||||
|
private readonly RedisConfig _config;
|
||||||
|
private readonly ILogger<DistributedWeightedRoundRobinPolicy> _logger;
|
||||||
|
|
||||||
|
public string Name => "DistributedWeightedRoundRobin";
|
||||||
|
|
||||||
|
public DistributedWeightedRoundRobinPolicy(
|
||||||
|
IConnectionMultiplexer redis,
|
||||||
|
RedisConfig config,
|
||||||
|
ILogger<DistributedWeightedRoundRobinPolicy> logger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_redis = redis;
|
||||||
|
_config = config;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DestinationState? PickDestination(
|
||||||
|
HttpContext context,
|
||||||
|
ClusterState cluster,
|
||||||
|
IReadOnlyList<DestinationState> availableDestinations
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (availableDestinations.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (availableDestinations.Count == 1)
|
||||||
|
return availableDestinations[0];
|
||||||
|
|
||||||
|
var clusterId = cluster.ClusterId;
|
||||||
|
var db = _redis.GetDatabase();
|
||||||
|
|
||||||
|
var lockKey = $"lock:{_config.InstanceName}:{clusterId}";
|
||||||
|
var stateKey = $"lb:{_config.InstanceName}:{clusterId}:state";
|
||||||
|
|
||||||
|
var lockValue = Guid.NewGuid().ToString();
|
||||||
|
var lockAcquired = db.StringSet(
|
||||||
|
lockKey,
|
||||||
|
lockValue,
|
||||||
|
TimeSpan.FromMilliseconds(500),
|
||||||
|
When.NotExists
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!lockAcquired)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Lock busy for cluster {Cluster}, using fallback selection",
|
||||||
|
clusterId
|
||||||
|
);
|
||||||
|
return FallbackSelection(availableDestinations);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var state = GetOrCreateLoadBalancingState(db, stateKey, availableDestinations);
|
||||||
|
|
||||||
|
var selectedDestination = SelectByWeight(state, availableDestinations);
|
||||||
|
|
||||||
|
UpdateCurrentWeights(db, stateKey, state, selectedDestination);
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Selected {Destination} for cluster {Cluster}",
|
||||||
|
selectedDestination?.DestinationId,
|
||||||
|
clusterId
|
||||||
|
);
|
||||||
|
|
||||||
|
return selectedDestination;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
ex,
|
||||||
|
"Error in distributed load balancing for cluster {Cluster}",
|
||||||
|
clusterId
|
||||||
|
);
|
||||||
|
return availableDestinations[0];
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
var script =
|
||||||
|
@"
|
||||||
|
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
||||||
|
return redis.call('DEL', KEYS[1])
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end";
|
||||||
|
db.ScriptEvaluate(script, new RedisKey[] { lockKey }, new RedisValue[] { lockValue });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoadBalancingState GetOrCreateLoadBalancingState(
|
||||||
|
IDatabase db,
|
||||||
|
string stateKey,
|
||||||
|
IReadOnlyList<DestinationState> destinations
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var existingState = db.StringGet(stateKey);
|
||||||
|
|
||||||
|
if (existingState.HasValue)
|
||||||
|
{
|
||||||
|
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||||
|
;
|
||||||
|
var parsedState = (LoadBalancingState?)
|
||||||
|
System.Text.Json.JsonSerializer.Deserialize<LoadBalancingState>(
|
||||||
|
existingState.ToString(),
|
||||||
|
options
|
||||||
|
);
|
||||||
|
var version = ComputeConfigHash(destinations);
|
||||||
|
|
||||||
|
if (
|
||||||
|
parsedState != null
|
||||||
|
&& parsedState.ConfigHash == version
|
||||||
|
&& parsedState.CurrentWeights != null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return parsedState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newState = new LoadBalancingState
|
||||||
|
{
|
||||||
|
ConfigHash = ComputeConfigHash(destinations),
|
||||||
|
CurrentWeights = new Dictionary<string, int>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var dest in destinations)
|
||||||
|
{
|
||||||
|
var weight = GetWeight(dest);
|
||||||
|
newState.CurrentWeights[dest.DestinationId] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = System.Text.Json.JsonSerializer.Serialize(newState);
|
||||||
|
db.StringSet(stateKey, json, TimeSpan.FromHours(1));
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long ComputeConfigHash(IReadOnlyList<DestinationState> destinations)
|
||||||
|
{
|
||||||
|
var hash = 0L;
|
||||||
|
foreach (var dest in destinations.OrderBy(d => d.DestinationId))
|
||||||
|
{
|
||||||
|
var weight = GetWeight(dest);
|
||||||
|
hash = HashCode.Combine(hash, dest.DestinationId.GetHashCode());
|
||||||
|
hash = HashCode.Combine(hash, weight);
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCurrentWeights(
|
||||||
|
IDatabase db,
|
||||||
|
string stateKey,
|
||||||
|
LoadBalancingState state,
|
||||||
|
DestinationState? selected
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (selected == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(state);
|
||||||
|
db.StringSet(stateKey, json, TimeSpan.FromHours(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private DestinationState? SelectByWeight(
|
||||||
|
LoadBalancingState state,
|
||||||
|
IReadOnlyList<DestinationState> destinations
|
||||||
|
)
|
||||||
|
{
|
||||||
|
int maxWeight = int.MinValue;
|
||||||
|
int totalWeight = 0;
|
||||||
|
DestinationState? selected = null;
|
||||||
|
|
||||||
|
foreach (var dest in destinations)
|
||||||
|
{
|
||||||
|
if (!state.CurrentWeights.ContainsKey(dest.DestinationId))
|
||||||
|
{
|
||||||
|
state.CurrentWeights[dest.DestinationId] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var weight = GetWeight(dest);
|
||||||
|
var currentWeight = state.CurrentWeights[dest.DestinationId];
|
||||||
|
|
||||||
|
var newWeight = currentWeight + weight;
|
||||||
|
state.CurrentWeights[dest.DestinationId] = newWeight;
|
||||||
|
totalWeight += weight;
|
||||||
|
|
||||||
|
if (newWeight > maxWeight)
|
||||||
|
{
|
||||||
|
maxWeight = newWeight;
|
||||||
|
selected = dest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected != null)
|
||||||
|
{
|
||||||
|
state.CurrentWeights[selected.DestinationId] = maxWeight - totalWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DestinationState? FallbackSelection(IReadOnlyList<DestinationState> destinations)
|
||||||
|
{
|
||||||
|
var hash = ComputeRequestHash();
|
||||||
|
var index = Math.Abs(hash % destinations.Count);
|
||||||
|
return destinations[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
private int ComputeRequestHash()
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
return HashCode.Combine(now.Second.GetHashCode(), now.Millisecond.GetHashCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetWeight(DestinationState destination)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
destination.Model?.Config?.Metadata?.TryGetValue("Weight", out var weightStr) == true
|
||||||
|
&& int.TryParse(weightStr, out var weight)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return weight;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LoadBalancingState
|
||||||
|
{
|
||||||
|
public long ConfigHash { get; set; }
|
||||||
|
public Dictionary<string, int> CurrentWeights { get; set; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
30
Metrics/GatewayMetrics.cs
Normal file
30
Metrics/GatewayMetrics.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using System.Diagnostics.Metrics;
|
||||||
|
|
||||||
|
namespace YarpGateway.Metrics;
|
||||||
|
|
||||||
|
public class GatewayMetrics
|
||||||
|
{
|
||||||
|
private readonly Counter<int> _requestsTotal;
|
||||||
|
private readonly Histogram<double> _requestDuration;
|
||||||
|
|
||||||
|
public GatewayMetrics(IMeterFactory meterFactory)
|
||||||
|
{
|
||||||
|
var meter = meterFactory.Create("fengling.gateway");
|
||||||
|
_requestsTotal = meter.CreateCounter<int>(
|
||||||
|
"gateway_requests_total",
|
||||||
|
"Total number of requests");
|
||||||
|
_requestDuration = meter.CreateHistogram<double>(
|
||||||
|
"gateway_request_duration_seconds",
|
||||||
|
"Request duration in seconds");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordRequest(string tenant, string service, int statusCode, double duration)
|
||||||
|
{
|
||||||
|
var tag = new KeyValuePair<string, object?>("tenant", tenant);
|
||||||
|
var tag2 = new KeyValuePair<string, object?>("service", service);
|
||||||
|
var tag3 = new KeyValuePair<string, object?>("status", statusCode.ToString());
|
||||||
|
|
||||||
|
_requestsTotal.Add(1, tag, tag2, tag3);
|
||||||
|
_requestDuration.Record(duration, tag, tag2);
|
||||||
|
}
|
||||||
|
}
|
||||||
83
Middleware/JwtTransformMiddleware.cs
Normal file
83
Middleware/JwtTransformMiddleware.cs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using YarpGateway.Config;
|
||||||
|
|
||||||
|
namespace YarpGateway.Middleware;
|
||||||
|
|
||||||
|
public class JwtTransformMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly JwtConfig _jwtConfig;
|
||||||
|
private readonly ILogger<JwtTransformMiddleware> _logger;
|
||||||
|
|
||||||
|
public JwtTransformMiddleware(
|
||||||
|
RequestDelegate next,
|
||||||
|
IOptions<JwtConfig> jwtConfig,
|
||||||
|
ILogger<JwtTransformMiddleware> logger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_jwtConfig = jwtConfig.Value;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
|
||||||
|
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer "))
|
||||||
|
{
|
||||||
|
await _next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = authHeader.Substring("Bearer ".Length).Trim();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jwtHandler = new JwtSecurityTokenHandler();
|
||||||
|
var jwtToken = jwtHandler.ReadJwtToken(token);
|
||||||
|
|
||||||
|
var tenantId = jwtToken.Claims.FirstOrDefault(c => c.Type == "tenant")?.Value;
|
||||||
|
var userId = jwtToken
|
||||||
|
.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)
|
||||||
|
?.Value;
|
||||||
|
var userName = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
|
||||||
|
var roles = jwtToken
|
||||||
|
.Claims.Where(c => c.Type == ClaimTypes.Role)
|
||||||
|
.Select(c => c.Value)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(tenantId))
|
||||||
|
{
|
||||||
|
context.Request.Headers["X-Tenant-Id"] = tenantId;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(userId))
|
||||||
|
context.Request.Headers["X-User-Id"] = userId;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(userName))
|
||||||
|
context.Request.Headers["X-User-Name"] = userName;
|
||||||
|
|
||||||
|
if (roles.Any())
|
||||||
|
context.Request.Headers["X-Roles"] = string.Join(",", roles);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"JWT transformed - Tenant: {Tenant}, User: {User}",
|
||||||
|
tenantId,
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("JWT missing tenant claim");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to parse JWT token");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
Middleware/TenantRoutingMiddleware.cs
Normal file
63
Middleware/TenantRoutingMiddleware.cs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using YarpGateway.Services;
|
||||||
|
|
||||||
|
namespace YarpGateway.Middleware;
|
||||||
|
|
||||||
|
public class TenantRoutingMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly IRouteCache _routeCache;
|
||||||
|
private readonly ILogger<TenantRoutingMiddleware> _logger;
|
||||||
|
|
||||||
|
public TenantRoutingMiddleware(
|
||||||
|
RequestDelegate next,
|
||||||
|
IRouteCache routeCache,
|
||||||
|
ILogger<TenantRoutingMiddleware> logger)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_routeCache = routeCache;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
|
||||||
|
if (string.IsNullOrEmpty(tenantId))
|
||||||
|
{
|
||||||
|
await _next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = context.Request.Path.Value ?? string.Empty;
|
||||||
|
var serviceName = ExtractServiceName(path);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(serviceName))
|
||||||
|
{
|
||||||
|
await _next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var route = _routeCache.GetRoute(tenantId, serviceName);
|
||||||
|
if (route == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Route not found - Tenant: {Tenant}, Service: {Service}", tenantId, serviceName);
|
||||||
|
await _next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Items["DynamicClusterId"] = route.ClusterId;
|
||||||
|
|
||||||
|
var routeType = route.IsGlobal ? "global" : "tenant-specific";
|
||||||
|
_logger.LogInformation("Tenant routing - Tenant: {Tenant}, Service: {Service}, Cluster: {Cluster}, Type: {Type}",
|
||||||
|
tenantId, serviceName, route.ClusterId, routeType);
|
||||||
|
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ExtractServiceName(string path)
|
||||||
|
{
|
||||||
|
var match = Regex.Match(path, @"/api/(\w+)/?");
|
||||||
|
return match.Success ? match.Groups[1].Value : string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
209
Migrations/20260201120312_InitialCreate.Designer.cs
generated
Normal file
209
Migrations/20260201120312_InitialCreate.Designer.cs
generated
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using YarpGateway.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace YarpGateway.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(GatewayDbContext))]
|
||||||
|
[Migration("20260201120312_InitialCreate")]
|
||||||
|
partial class InitialCreate
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("YarpGateway.Models.GwServiceInstance", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Address")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<string>("ClusterId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Health")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Version")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Weight")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Health");
|
||||||
|
|
||||||
|
b.HasIndex("ClusterId", "DestinationId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("ServiceInstances");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("YarpGateway.Models.GwTenant", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("TenantCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("TenantName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Version")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantCode")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Tenants");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("YarpGateway.Models.GwTenantRoute", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClusterId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("PathPattern")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<int>("Priority")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("ServiceName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("TenantCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Version")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ClusterId");
|
||||||
|
|
||||||
|
b.HasIndex("TenantCode", "ServiceName")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("TenantRoutes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("YarpGateway.Models.GwTenantRoute", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("YarpGateway.Models.GwTenant", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantCode")
|
||||||
|
.HasPrincipalKey("TenantCode")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
133
Migrations/20260201120312_InitialCreate.cs
Normal file
133
Migrations/20260201120312_InitialCreate.cs
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace YarpGateway.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InitialCreate : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ServiceInstances",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
ClusterId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
DestinationId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
Address = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||||
|
Health = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Weight = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Status = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
CreatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
UpdatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
UpdatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
Version = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ServiceInstances", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Tenants",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
TenantCode = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||||
|
TenantName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
Status = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
CreatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
UpdatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
UpdatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
Version = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Tenants", x => x.Id);
|
||||||
|
table.UniqueConstraint("AK_Tenants_TenantCode", x => x.TenantCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TenantRoutes",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
TenantCode = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||||
|
ServiceName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
ClusterId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
PathPattern = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||||
|
Priority = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Status = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
CreatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
UpdatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
UpdatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
Version = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TenantRoutes", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TenantRoutes_Tenants_TenantCode",
|
||||||
|
column: x => x.TenantCode,
|
||||||
|
principalTable: "Tenants",
|
||||||
|
principalColumn: "TenantCode",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ServiceInstances_ClusterId_DestinationId",
|
||||||
|
table: "ServiceInstances",
|
||||||
|
columns: new[] { "ClusterId", "DestinationId" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ServiceInstances_Health",
|
||||||
|
table: "ServiceInstances",
|
||||||
|
column: "Health");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TenantRoutes_ClusterId",
|
||||||
|
table: "TenantRoutes",
|
||||||
|
column: "ClusterId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TenantRoutes_TenantCode_ServiceName",
|
||||||
|
table: "TenantRoutes",
|
||||||
|
columns: new[] { "TenantCode", "ServiceName" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Tenants_TenantCode",
|
||||||
|
table: "Tenants",
|
||||||
|
column: "TenantCode",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ServiceInstances");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TenantRoutes");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Tenants");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
205
Migrations/20260201133826_AddIsGlobalToTenantRoute.Designer.cs
generated
Normal file
205
Migrations/20260201133826_AddIsGlobalToTenantRoute.Designer.cs
generated
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using YarpGateway.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace YarpGateway.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(GatewayDbContext))]
|
||||||
|
[Migration("20260201133826_AddIsGlobalToTenantRoute")]
|
||||||
|
partial class AddIsGlobalToTenantRoute
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("YarpGateway.Models.GwServiceInstance", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Address")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<string>("ClusterId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Health")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Version")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Weight")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Health");
|
||||||
|
|
||||||
|
b.HasIndex("ClusterId", "DestinationId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("ServiceInstances");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("YarpGateway.Models.GwTenant", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("TenantCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("TenantName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Version")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantCode")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Tenants");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("YarpGateway.Models.GwTenantRoute", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClusterId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsGlobal")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("PathPattern")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<int>("Priority")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("ServiceName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("TenantCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Version")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ClusterId");
|
||||||
|
|
||||||
|
b.HasIndex("ServiceName");
|
||||||
|
|
||||||
|
b.HasIndex("TenantCode");
|
||||||
|
|
||||||
|
b.HasIndex("ServiceName", "IsGlobal", "Status");
|
||||||
|
|
||||||
|
b.ToTable("TenantRoutes");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
87
Migrations/20260201133826_AddIsGlobalToTenantRoute.cs
Normal file
87
Migrations/20260201133826_AddIsGlobalToTenantRoute.cs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace YarpGateway.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddIsGlobalToTenantRoute : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_TenantRoutes_Tenants_TenantCode",
|
||||||
|
table: "TenantRoutes");
|
||||||
|
|
||||||
|
migrationBuilder.DropUniqueConstraint(
|
||||||
|
name: "AK_Tenants_TenantCode",
|
||||||
|
table: "Tenants");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_TenantRoutes_TenantCode_ServiceName",
|
||||||
|
table: "TenantRoutes");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsGlobal",
|
||||||
|
table: "TenantRoutes",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TenantRoutes_ServiceName",
|
||||||
|
table: "TenantRoutes",
|
||||||
|
column: "ServiceName");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TenantRoutes_ServiceName_IsGlobal_Status",
|
||||||
|
table: "TenantRoutes",
|
||||||
|
columns: new[] { "ServiceName", "IsGlobal", "Status" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TenantRoutes_TenantCode",
|
||||||
|
table: "TenantRoutes",
|
||||||
|
column: "TenantCode");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_TenantRoutes_ServiceName",
|
||||||
|
table: "TenantRoutes");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_TenantRoutes_ServiceName_IsGlobal_Status",
|
||||||
|
table: "TenantRoutes");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_TenantRoutes_TenantCode",
|
||||||
|
table: "TenantRoutes");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsGlobal",
|
||||||
|
table: "TenantRoutes");
|
||||||
|
|
||||||
|
migrationBuilder.AddUniqueConstraint(
|
||||||
|
name: "AK_Tenants_TenantCode",
|
||||||
|
table: "Tenants",
|
||||||
|
column: "TenantCode");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TenantRoutes_TenantCode_ServiceName",
|
||||||
|
table: "TenantRoutes",
|
||||||
|
columns: new[] { "TenantCode", "ServiceName" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_TenantRoutes_Tenants_TenantCode",
|
||||||
|
table: "TenantRoutes",
|
||||||
|
column: "TenantCode",
|
||||||
|
principalTable: "Tenants",
|
||||||
|
principalColumn: "TenantCode",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
202
Migrations/GatewayDbContextModelSnapshot.cs
Normal file
202
Migrations/GatewayDbContextModelSnapshot.cs
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using YarpGateway.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace YarpGateway.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(GatewayDbContext))]
|
||||||
|
partial class GatewayDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("YarpGateway.Models.GwServiceInstance", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Address")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<string>("ClusterId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Health")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Version")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Weight")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Health");
|
||||||
|
|
||||||
|
b.HasIndex("ClusterId", "DestinationId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("ServiceInstances");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("YarpGateway.Models.GwTenant", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("TenantCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("TenantName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Version")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantCode")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Tenants");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("YarpGateway.Models.GwTenantRoute", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClusterId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsGlobal")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("PathPattern")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<int>("Priority")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("ServiceName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("TenantCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Version")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ClusterId");
|
||||||
|
|
||||||
|
b.HasIndex("ServiceName");
|
||||||
|
|
||||||
|
b.HasIndex("TenantCode");
|
||||||
|
|
||||||
|
b.HasIndex("ServiceName", "IsGlobal", "Status");
|
||||||
|
|
||||||
|
b.ToTable("TenantRoutes");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
Migrations/script.sql
Normal file
89
Migrations/script.sql
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
||||||
|
"MigrationId" character varying(150) NOT NULL,
|
||||||
|
"ProductVersion" character varying(32) NOT NULL,
|
||||||
|
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
|
||||||
|
);
|
||||||
|
|
||||||
|
START TRANSACTION;
|
||||||
|
CREATE TABLE "ServiceInstances" (
|
||||||
|
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
|
||||||
|
"ClusterId" character varying(100) NOT NULL,
|
||||||
|
"DestinationId" character varying(100) NOT NULL,
|
||||||
|
"Address" character varying(200) NOT NULL,
|
||||||
|
"Health" integer NOT NULL,
|
||||||
|
"Weight" integer NOT NULL,
|
||||||
|
"Status" integer NOT NULL,
|
||||||
|
"CreatedBy" bigint,
|
||||||
|
"CreatedTime" timestamp with time zone NOT NULL,
|
||||||
|
"UpdatedBy" bigint,
|
||||||
|
"UpdatedTime" timestamp with time zone,
|
||||||
|
"IsDeleted" boolean NOT NULL,
|
||||||
|
"Version" integer NOT NULL,
|
||||||
|
CONSTRAINT "PK_ServiceInstances" PRIMARY KEY ("Id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "Tenants" (
|
||||||
|
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
|
||||||
|
"TenantCode" character varying(50) NOT NULL,
|
||||||
|
"TenantName" character varying(100) NOT NULL,
|
||||||
|
"Status" integer NOT NULL,
|
||||||
|
"CreatedBy" bigint,
|
||||||
|
"CreatedTime" timestamp with time zone NOT NULL,
|
||||||
|
"UpdatedBy" bigint,
|
||||||
|
"UpdatedTime" timestamp with time zone,
|
||||||
|
"IsDeleted" boolean NOT NULL,
|
||||||
|
"Version" integer NOT NULL,
|
||||||
|
CONSTRAINT "PK_Tenants" PRIMARY KEY ("Id"),
|
||||||
|
CONSTRAINT "AK_Tenants_TenantCode" UNIQUE ("TenantCode")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "TenantRoutes" (
|
||||||
|
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
|
||||||
|
"TenantCode" character varying(50) NOT NULL,
|
||||||
|
"ServiceName" character varying(100) NOT NULL,
|
||||||
|
"ClusterId" character varying(100) NOT NULL,
|
||||||
|
"PathPattern" character varying(200) NOT NULL,
|
||||||
|
"Priority" integer NOT NULL,
|
||||||
|
"Status" integer NOT NULL,
|
||||||
|
"CreatedBy" bigint,
|
||||||
|
"CreatedTime" timestamp with time zone NOT NULL,
|
||||||
|
"UpdatedBy" bigint,
|
||||||
|
"UpdatedTime" timestamp with time zone,
|
||||||
|
"IsDeleted" boolean NOT NULL,
|
||||||
|
"Version" integer NOT NULL,
|
||||||
|
CONSTRAINT "PK_TenantRoutes" PRIMARY KEY ("Id"),
|
||||||
|
CONSTRAINT "FK_TenantRoutes_Tenants_TenantCode" FOREIGN KEY ("TenantCode") REFERENCES "Tenants" ("TenantCode") ON DELETE RESTRICT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "IX_ServiceInstances_ClusterId_DestinationId" ON "ServiceInstances" ("ClusterId", "DestinationId");
|
||||||
|
|
||||||
|
CREATE INDEX "IX_ServiceInstances_Health" ON "ServiceInstances" ("Health");
|
||||||
|
|
||||||
|
CREATE INDEX "IX_TenantRoutes_ClusterId" ON "TenantRoutes" ("ClusterId");
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "IX_TenantRoutes_TenantCode_ServiceName" ON "TenantRoutes" ("TenantCode", "ServiceName");
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "IX_Tenants_TenantCode" ON "Tenants" ("TenantCode");
|
||||||
|
|
||||||
|
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||||
|
VALUES ('20260201120312_InitialCreate', '9.0.0');
|
||||||
|
|
||||||
|
ALTER TABLE "TenantRoutes" DROP CONSTRAINT "FK_TenantRoutes_Tenants_TenantCode";
|
||||||
|
|
||||||
|
ALTER TABLE "Tenants" DROP CONSTRAINT "AK_Tenants_TenantCode";
|
||||||
|
|
||||||
|
DROP INDEX "IX_TenantRoutes_TenantCode_ServiceName";
|
||||||
|
|
||||||
|
ALTER TABLE "TenantRoutes" ADD "IsGlobal" boolean NOT NULL DEFAULT FALSE;
|
||||||
|
|
||||||
|
CREATE INDEX "IX_TenantRoutes_ServiceName" ON "TenantRoutes" ("ServiceName");
|
||||||
|
|
||||||
|
CREATE INDEX "IX_TenantRoutes_ServiceName_IsGlobal_Status" ON "TenantRoutes" ("ServiceName", "IsGlobal", "Status");
|
||||||
|
|
||||||
|
CREATE INDEX "IX_TenantRoutes_TenantCode" ON "TenantRoutes" ("TenantCode");
|
||||||
|
|
||||||
|
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||||
|
VALUES ('20260201133826_AddIsGlobalToTenantRoute', '9.0.0');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
18
Models/GwServiceInstance.cs
Normal file
18
Models/GwServiceInstance.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
namespace YarpGateway.Models;
|
||||||
|
|
||||||
|
public class GwServiceInstance
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string ClusterId { get; set; } = string.Empty;
|
||||||
|
public string DestinationId { get; set; } = string.Empty;
|
||||||
|
public string Address { get; set; } = string.Empty;
|
||||||
|
public int Health { get; set; } = 1;
|
||||||
|
public int Weight { get; set; } = 1;
|
||||||
|
public int Status { get; set; } = 1;
|
||||||
|
public long? CreatedBy { get; set; }
|
||||||
|
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
|
||||||
|
public long? UpdatedBy { get; set; }
|
||||||
|
public DateTime? UpdatedTime { get; set; }
|
||||||
|
public bool IsDeleted { get; set; } = false;
|
||||||
|
public int Version { get; set; } = 0;
|
||||||
|
}
|
||||||
15
Models/GwTenant.cs
Normal file
15
Models/GwTenant.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
namespace YarpGateway.Models;
|
||||||
|
|
||||||
|
public class GwTenant
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string TenantCode { get; set; } = string.Empty;
|
||||||
|
public string TenantName { get; set; } = string.Empty;
|
||||||
|
public int Status { get; set; } = 1;
|
||||||
|
public long? CreatedBy { get; set; }
|
||||||
|
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
|
||||||
|
public long? UpdatedBy { get; set; }
|
||||||
|
public DateTime? UpdatedTime { get; set; }
|
||||||
|
public bool IsDeleted { get; set; } = false;
|
||||||
|
public int Version { get; set; } = 0;
|
||||||
|
}
|
||||||
19
Models/GwTenantRoute.cs
Normal file
19
Models/GwTenantRoute.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
namespace YarpGateway.Models;
|
||||||
|
|
||||||
|
public class GwTenantRoute
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string TenantCode { get; set; } = string.Empty;
|
||||||
|
public string ServiceName { get; set; } = string.Empty;
|
||||||
|
public string ClusterId { get; set; } = string.Empty;
|
||||||
|
public string PathPattern { get; set; } = string.Empty;
|
||||||
|
public int Priority { get; set; } = 0;
|
||||||
|
public int Status { get; set; } = 1;
|
||||||
|
public bool IsGlobal { get; set; } = false;
|
||||||
|
public long? CreatedBy { get; set; }
|
||||||
|
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
|
||||||
|
public long? UpdatedBy { get; set; }
|
||||||
|
public DateTime? UpdatedTime { get; set; }
|
||||||
|
public bool IsDeleted { get; set; } = false;
|
||||||
|
public int Version { get; set; } = 0;
|
||||||
|
}
|
||||||
113
Program.cs
Normal file
113
Program.cs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Serilog;
|
||||||
|
using Yarp.ReverseProxy.Configuration;
|
||||||
|
using Yarp.ReverseProxy.LoadBalancing;
|
||||||
|
using YarpGateway.Config;
|
||||||
|
using YarpGateway.Data;
|
||||||
|
using YarpGateway.DynamicProxy;
|
||||||
|
using YarpGateway.LoadBalancing;
|
||||||
|
using YarpGateway.Middleware;
|
||||||
|
using YarpGateway.Services;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Host.UseSerilog(
|
||||||
|
(context, services, configuration) =>
|
||||||
|
configuration
|
||||||
|
.ReadFrom.Configuration(context.Configuration)
|
||||||
|
.ReadFrom.Services(services)
|
||||||
|
.Enrich.FromLogContext()
|
||||||
|
);
|
||||||
|
|
||||||
|
builder.Services.Configure<JwtConfig>(builder.Configuration.GetSection("Jwt"));
|
||||||
|
builder.Services.Configure<RedisConfig>(builder.Configuration.GetSection("Redis"));
|
||||||
|
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<RedisConfig>>().Value);
|
||||||
|
|
||||||
|
builder.Services.AddDbContextFactory<GatewayDbContext>(options =>
|
||||||
|
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))
|
||||||
|
);
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<DatabaseRouteConfigProvider>();
|
||||||
|
builder.Services.AddSingleton<DatabaseClusterConfigProvider>();
|
||||||
|
builder.Services.AddSingleton<IRouteCache, RouteCache>();
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<IRedisConnectionManager, RedisConnectionManager>();
|
||||||
|
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
|
||||||
|
{
|
||||||
|
var config = sp.GetRequiredService<RedisConfig>();
|
||||||
|
var connectionOptions = ConfigurationOptions.Parse(config.ConnectionString);
|
||||||
|
connectionOptions.AbortOnConnectFail = false;
|
||||||
|
connectionOptions.ConnectRetry = 3;
|
||||||
|
connectionOptions.ConnectTimeout = 5000;
|
||||||
|
connectionOptions.SyncTimeout = 3000;
|
||||||
|
connectionOptions.DefaultDatabase = config.Database;
|
||||||
|
|
||||||
|
var connection = ConnectionMultiplexer.Connect(connectionOptions);
|
||||||
|
connection.ConnectionFailed += (sender, e) =>
|
||||||
|
{
|
||||||
|
Serilog.Log.Error(e.Exception, "Redis connection failed");
|
||||||
|
};
|
||||||
|
connection.ConnectionRestored += (sender, e) =>
|
||||||
|
{
|
||||||
|
Serilog.Log.Information("Redis connection restored");
|
||||||
|
};
|
||||||
|
|
||||||
|
return connection;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<ILoadBalancingPolicy, DistributedWeightedRoundRobinPolicy>();
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<DynamicProxyConfigProvider>();
|
||||||
|
builder.Services.AddSingleton<IProxyConfigProvider>(sp => sp.GetRequiredService<DynamicProxyConfigProvider>());
|
||||||
|
|
||||||
|
var corsSettings = builder.Configuration.GetSection("Cors");
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
var allowAnyOrigin = corsSettings.GetValue<bool>("AllowAnyOrigin");
|
||||||
|
var allowedOrigins = corsSettings.GetSection("AllowedOrigins").Get<string[]>() ?? Array.Empty<string>();
|
||||||
|
|
||||||
|
options.AddPolicy("AllowFrontend", policy =>
|
||||||
|
{
|
||||||
|
if (allowAnyOrigin)
|
||||||
|
{
|
||||||
|
policy.AllowAnyOrigin();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
policy.WithOrigins(allowedOrigins);
|
||||||
|
}
|
||||||
|
|
||||||
|
policy.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowCredentials();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseCors("AllowFrontend");
|
||||||
|
app.UseMiddleware<JwtTransformMiddleware>();
|
||||||
|
app.UseMiddleware<TenantRoutingMiddleware>();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
app.MapReverseProxy();
|
||||||
|
|
||||||
|
await app.Services.GetRequiredService<IRouteCache>().InitializeAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log.Information("Starting YARP Gateway");
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Fatal(ex, "Application terminated unexpectedly");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Log.CloseAndFlush();
|
||||||
|
}
|
||||||
14
Properties/launchSettings.json
Normal file
14
Properties/launchSettings.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "http://localhost:5046",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
138
Services/RedisConnectionManager.cs
Normal file
138
Services/RedisConnectionManager.cs
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
using StackExchange.Redis;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using YarpGateway.Config;
|
||||||
|
|
||||||
|
namespace YarpGateway.Services;
|
||||||
|
|
||||||
|
public interface IRedisConnectionManager
|
||||||
|
{
|
||||||
|
IConnectionMultiplexer GetConnection();
|
||||||
|
Task<IDisposable> AcquireLockAsync(string key, TimeSpan? expiry = null);
|
||||||
|
Task<T> ExecuteInLockAsync<T>(string key, Func<Task<T>> func, TimeSpan? expiry = null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RedisConnectionManager : IRedisConnectionManager
|
||||||
|
{
|
||||||
|
private readonly Lazy<IConnectionMultiplexer> _lazyConnection;
|
||||||
|
private readonly RedisConfig _config;
|
||||||
|
private readonly ILogger<RedisConnectionManager> _logger;
|
||||||
|
|
||||||
|
public RedisConnectionManager(RedisConfig config, ILogger<RedisConnectionManager> logger)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_logger = logger;
|
||||||
|
_lazyConnection = new Lazy<IConnectionMultiplexer>(() =>
|
||||||
|
{
|
||||||
|
var configuration = ConfigurationOptions.Parse(_config.ConnectionString);
|
||||||
|
configuration.AbortOnConnectFail = false;
|
||||||
|
configuration.ConnectRetry = 3;
|
||||||
|
configuration.ConnectTimeout = 5000;
|
||||||
|
configuration.SyncTimeout = 3000;
|
||||||
|
configuration.DefaultDatabase = _config.Database;
|
||||||
|
|
||||||
|
var connection = ConnectionMultiplexer.Connect(configuration);
|
||||||
|
connection.ConnectionRestored += (sender, e) =>
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Redis connection restored");
|
||||||
|
};
|
||||||
|
connection.ConnectionFailed += (sender, e) =>
|
||||||
|
{
|
||||||
|
_logger.LogError(e.Exception, "Redis connection failed");
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("Connected to Redis at {ConnectionString}", _config.ConnectionString);
|
||||||
|
return connection;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public IConnectionMultiplexer GetConnection()
|
||||||
|
{
|
||||||
|
return _lazyConnection.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IDisposable> AcquireLockAsync(string key, TimeSpan? expiry = null)
|
||||||
|
{
|
||||||
|
var expiryTime = expiry ?? TimeSpan.FromSeconds(10);
|
||||||
|
var redis = GetConnection();
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var lockKey = $"lock:{_config.InstanceName}:{key}";
|
||||||
|
|
||||||
|
var lockValue = Environment.MachineName + ":" + Process.GetCurrentProcess().Id;
|
||||||
|
var acquired = await db.StringSetAsync(lockKey, lockValue, expiryTime, When.NotExists);
|
||||||
|
|
||||||
|
if (!acquired)
|
||||||
|
{
|
||||||
|
var backoff = TimeSpan.FromMilliseconds(100);
|
||||||
|
var retryCount = 0;
|
||||||
|
const int maxRetries = 50;
|
||||||
|
|
||||||
|
while (!acquired && retryCount < maxRetries)
|
||||||
|
{
|
||||||
|
await Task.Delay(backoff);
|
||||||
|
acquired = await db.StringSetAsync(lockKey, lockValue, expiryTime, When.NotExists);
|
||||||
|
retryCount++;
|
||||||
|
if (retryCount < 10)
|
||||||
|
{
|
||||||
|
backoff = TimeSpan.FromMilliseconds(100 * (retryCount + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!acquired)
|
||||||
|
{
|
||||||
|
throw new TimeoutException($"Failed to acquire lock for key: {lockKey}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RedisLock(db, lockKey, lockValue, _logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T> ExecuteInLockAsync<T>(string key, Func<Task<T>> func, TimeSpan? expiry = null)
|
||||||
|
{
|
||||||
|
using var @lock = await AcquireLockAsync(key, expiry);
|
||||||
|
return await func();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RedisLock : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IDatabase _db;
|
||||||
|
private readonly string _key;
|
||||||
|
private readonly string _value;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public RedisLock(IDatabase db, string key, string value, ILogger logger)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_key = key;
|
||||||
|
_value = value;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var script = @"
|
||||||
|
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
||||||
|
return redis.call('DEL', KEYS[1])
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end";
|
||||||
|
|
||||||
|
_db.ScriptEvaluate(script, new RedisKey[] { _key }, new RedisValue[] { _value });
|
||||||
|
_logger.LogDebug("Released lock for key: {Key}", _key);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to release lock for key: {Key}", _key);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
138
Services/RouteCache.cs
Normal file
138
Services/RouteCache.cs
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using YarpGateway.Models;
|
||||||
|
using YarpGateway.Data;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace YarpGateway.Services;
|
||||||
|
|
||||||
|
public class RouteInfo
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string ClusterId { get; set; } = string.Empty;
|
||||||
|
public string PathPattern { get; set; } = string.Empty;
|
||||||
|
public int Priority { get; set; }
|
||||||
|
public bool IsGlobal { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IRouteCache
|
||||||
|
{
|
||||||
|
Task InitializeAsync();
|
||||||
|
Task ReloadAsync();
|
||||||
|
RouteInfo? GetRoute(string tenantCode, string serviceName);
|
||||||
|
RouteInfo? GetRouteByPath(string path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RouteCache : IRouteCache
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<GatewayDbContext> _dbContextFactory;
|
||||||
|
private readonly ILogger<RouteCache> _logger;
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, RouteInfo> _globalRoutes = new();
|
||||||
|
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, RouteInfo>> _tenantRoutes = new();
|
||||||
|
private readonly ConcurrentDictionary<string, RouteInfo> _pathRoutes = new();
|
||||||
|
private readonly ReaderWriterLockSlim _lock = new();
|
||||||
|
|
||||||
|
public RouteCache(IDbContextFactory<GatewayDbContext> dbContextFactory, ILogger<RouteCache> logger)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Initializing route cache from database...");
|
||||||
|
await LoadFromDatabaseAsync();
|
||||||
|
_logger.LogInformation("Route cache initialized: {GlobalCount} global routes, {TenantCount} tenant routes",
|
||||||
|
_globalRoutes.Count, _tenantRoutes.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ReloadAsync()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Reloading route cache...");
|
||||||
|
await LoadFromDatabaseAsync();
|
||||||
|
_logger.LogInformation("Route cache reloaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouteInfo? GetRoute(string tenantCode, string serviceName)
|
||||||
|
{
|
||||||
|
_lock.EnterUpgradeableReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 1. 优先查找租户专用路由
|
||||||
|
if (_tenantRoutes.TryGetValue(tenantCode, out var tenantRouteMap) &&
|
||||||
|
tenantRouteMap.TryGetValue(serviceName, out var tenantRoute))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Found tenant-specific route: {Tenant}/{Service} -> {Cluster}",
|
||||||
|
tenantCode, serviceName, tenantRoute.ClusterId);
|
||||||
|
return tenantRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 查找全局路由
|
||||||
|
if (_globalRoutes.TryGetValue(serviceName, out var globalRoute))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Found global route: {Service} -> {Cluster} for tenant {Tenant}",
|
||||||
|
serviceName, globalRoute.ClusterId, tenantCode);
|
||||||
|
return globalRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 没找到
|
||||||
|
_logger.LogWarning("No route found for: {Tenant}/{Service}", tenantCode, serviceName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_lock.ExitUpgradeableReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouteInfo? GetRouteByPath(string path)
|
||||||
|
{
|
||||||
|
return _pathRoutes.TryGetValue(path, out var route) ? route : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadFromDatabaseAsync()
|
||||||
|
{
|
||||||
|
using var db = _dbContextFactory.CreateDbContext();
|
||||||
|
|
||||||
|
var routes = await db.TenantRoutes
|
||||||
|
.Where(r => r.Status == 1 && !r.IsDeleted)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
_lock.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_globalRoutes.Clear();
|
||||||
|
_tenantRoutes.Clear();
|
||||||
|
_pathRoutes.Clear();
|
||||||
|
|
||||||
|
foreach (var route in routes)
|
||||||
|
{
|
||||||
|
var routeInfo = new RouteInfo
|
||||||
|
{
|
||||||
|
Id = route.Id,
|
||||||
|
ClusterId = route.ClusterId,
|
||||||
|
PathPattern = route.PathPattern,
|
||||||
|
Priority = route.Priority,
|
||||||
|
IsGlobal = route.IsGlobal
|
||||||
|
};
|
||||||
|
|
||||||
|
if (route.IsGlobal)
|
||||||
|
{
|
||||||
|
_globalRoutes[route.ServiceName] = routeInfo;
|
||||||
|
_pathRoutes[route.PathPattern] = routeInfo;
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(route.TenantCode))
|
||||||
|
{
|
||||||
|
_tenantRoutes.GetOrAdd(route.TenantCode, _ => new())
|
||||||
|
[route.ServiceName] = routeInfo;
|
||||||
|
_pathRoutes[route.PathPattern] = routeInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_lock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
YarpGateway.csproj
Normal file
23
YarpGateway.csproj
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
|
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
|
||||||
|
<PackageReference Include="Yarp.ReverseProxy" Version="2.2.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
8
appsettings.Development.json
Normal file
8
appsettings.Development.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
appsettings.json
Normal file
64
appsettings.json
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"Yarp.ReverseProxy": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"Cors": {
|
||||||
|
"AllowedOrigins": [
|
||||||
|
"http://localhost:5173",
|
||||||
|
"http://127.0.0.1:5173",
|
||||||
|
"http://localhost:5174"
|
||||||
|
],
|
||||||
|
"AllowAnyOrigin": false
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "Host=192.168.100.10;Port=5432;Database=fengling_gateway;Username=movingsam;Password=sl52788542"
|
||||||
|
},
|
||||||
|
"Jwt": {
|
||||||
|
"Authority": "https://your-auth-server.com",
|
||||||
|
"Audience": "fengling-gateway",
|
||||||
|
"ValidateIssuer": true,
|
||||||
|
"ValidateAudience": true
|
||||||
|
},
|
||||||
|
"Redis": {
|
||||||
|
"ConnectionString": "192.168.100.10:6379",
|
||||||
|
"Database": 0,
|
||||||
|
"InstanceName": "YarpGateway"
|
||||||
|
},
|
||||||
|
"ReverseProxy": {
|
||||||
|
"Routes": {},
|
||||||
|
"Clusters": {}
|
||||||
|
},
|
||||||
|
"Serilog": {
|
||||||
|
"Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"],
|
||||||
|
"MinimumLevel": "Information",
|
||||||
|
"WriteTo": [
|
||||||
|
{
|
||||||
|
"Name": "Console",
|
||||||
|
"Args": {
|
||||||
|
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "File",
|
||||||
|
"Args": {
|
||||||
|
"path": "logs/gateway-.log",
|
||||||
|
"rollingInterval": "Day",
|
||||||
|
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
|
||||||
|
},
|
||||||
|
"Kestrel": {
|
||||||
|
"Endpoints": {
|
||||||
|
"Http": {
|
||||||
|
"Url": "http://0.0.0.0:8080"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
bin/Debug/net10.0/Humanizer.dll
Executable file
BIN
bin/Debug/net10.0/Humanizer.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.AspNetCore.Authentication.JwtBearer.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.AspNetCore.Authentication.JwtBearer.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.Bcl.AsyncInterfaces.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.Bcl.AsyncInterfaces.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.Build.Locator.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.Build.Locator.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.CodeAnalysis.CSharp.Workspaces.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.CodeAnalysis.CSharp.Workspaces.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.CodeAnalysis.CSharp.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.CodeAnalysis.CSharp.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.CodeAnalysis.Workspaces.MSBuild.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.CodeAnalysis.Workspaces.MSBuild.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.CodeAnalysis.Workspaces.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.CodeAnalysis.Workspaces.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.CodeAnalysis.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.CodeAnalysis.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.EntityFrameworkCore.Abstractions.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.EntityFrameworkCore.Abstractions.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.EntityFrameworkCore.Design.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.EntityFrameworkCore.Design.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.EntityFrameworkCore.Relational.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.EntityFrameworkCore.Relational.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.EntityFrameworkCore.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.EntityFrameworkCore.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.Extensions.DependencyModel.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.Extensions.DependencyModel.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.IdentityModel.Abstractions.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.IdentityModel.Abstractions.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.IdentityModel.JsonWebTokens.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.IdentityModel.JsonWebTokens.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.IdentityModel.Logging.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.IdentityModel.Logging.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.IdentityModel.Protocols.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.IdentityModel.Protocols.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Microsoft.IdentityModel.Tokens.dll
Executable file
BIN
bin/Debug/net10.0/Microsoft.IdentityModel.Tokens.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Mono.TextTemplating.dll
Executable file
BIN
bin/Debug/net10.0/Mono.TextTemplating.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Npgsql.EntityFrameworkCore.PostgreSQL.dll
Executable file
BIN
bin/Debug/net10.0/Npgsql.EntityFrameworkCore.PostgreSQL.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Npgsql.dll
Executable file
BIN
bin/Debug/net10.0/Npgsql.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Pipelines.Sockets.Unofficial.dll
Executable file
BIN
bin/Debug/net10.0/Pipelines.Sockets.Unofficial.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Serilog.AspNetCore.dll
Executable file
BIN
bin/Debug/net10.0/Serilog.AspNetCore.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Serilog.Extensions.Hosting.dll
Executable file
BIN
bin/Debug/net10.0/Serilog.Extensions.Hosting.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Serilog.Extensions.Logging.dll
Executable file
BIN
bin/Debug/net10.0/Serilog.Extensions.Logging.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Serilog.Formatting.Compact.dll
Executable file
BIN
bin/Debug/net10.0/Serilog.Formatting.Compact.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Serilog.Settings.Configuration.dll
Executable file
BIN
bin/Debug/net10.0/Serilog.Settings.Configuration.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Serilog.Sinks.Console.dll
Executable file
BIN
bin/Debug/net10.0/Serilog.Sinks.Console.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Serilog.Sinks.Debug.dll
Executable file
BIN
bin/Debug/net10.0/Serilog.Sinks.Debug.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Serilog.Sinks.File.dll
Executable file
BIN
bin/Debug/net10.0/Serilog.Sinks.File.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Serilog.dll
Executable file
BIN
bin/Debug/net10.0/Serilog.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/StackExchange.Redis.dll
Executable file
BIN
bin/Debug/net10.0/StackExchange.Redis.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/System.CodeDom.dll
Executable file
BIN
bin/Debug/net10.0/System.CodeDom.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/System.Composition.AttributedModel.dll
Executable file
BIN
bin/Debug/net10.0/System.Composition.AttributedModel.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/System.Composition.Convention.dll
Executable file
BIN
bin/Debug/net10.0/System.Composition.Convention.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/System.Composition.Hosting.dll
Executable file
BIN
bin/Debug/net10.0/System.Composition.Hosting.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/System.Composition.Runtime.dll
Executable file
BIN
bin/Debug/net10.0/System.Composition.Runtime.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/System.Composition.TypedParts.dll
Executable file
BIN
bin/Debug/net10.0/System.Composition.TypedParts.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/System.IO.Hashing.dll
Executable file
BIN
bin/Debug/net10.0/System.IO.Hashing.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/System.IdentityModel.Tokens.Jwt.dll
Executable file
BIN
bin/Debug/net10.0/System.IdentityModel.Tokens.Jwt.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/Yarp.ReverseProxy.dll
Executable file
BIN
bin/Debug/net10.0/Yarp.ReverseProxy.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/YarpGateway
Executable file
BIN
bin/Debug/net10.0/YarpGateway
Executable file
Binary file not shown.
1019
bin/Debug/net10.0/YarpGateway.deps.json
Normal file
1019
bin/Debug/net10.0/YarpGateway.deps.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
bin/Debug/net10.0/YarpGateway.dll
Normal file
BIN
bin/Debug/net10.0/YarpGateway.dll
Normal file
Binary file not shown.
BIN
bin/Debug/net10.0/YarpGateway.pdb
Normal file
BIN
bin/Debug/net10.0/YarpGateway.pdb
Normal file
Binary file not shown.
20
bin/Debug/net10.0/YarpGateway.runtimeconfig.json
Normal file
20
bin/Debug/net10.0/YarpGateway.runtimeconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"runtimeOptions": {
|
||||||
|
"tfm": "net10.0",
|
||||||
|
"frameworks": [
|
||||||
|
{
|
||||||
|
"name": "Microsoft.NETCore.App",
|
||||||
|
"version": "10.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Microsoft.AspNetCore.App",
|
||||||
|
"version": "10.0.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configProperties": {
|
||||||
|
"System.GC.Server": true,
|
||||||
|
"System.Reflection.NullabilityInfoContext.IsSupported": true,
|
||||||
|
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"Version":1,"ManifestType":"Build","Endpoints":[]}
|
||||||
8
bin/Debug/net10.0/appsettings.Development.json
Normal file
8
bin/Debug/net10.0/appsettings.Development.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
bin/Debug/net10.0/appsettings.json
Normal file
64
bin/Debug/net10.0/appsettings.json
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"Yarp.ReverseProxy": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"Cors": {
|
||||||
|
"AllowedOrigins": [
|
||||||
|
"http://localhost:5173",
|
||||||
|
"http://127.0.0.1:5173",
|
||||||
|
"http://localhost:5174"
|
||||||
|
],
|
||||||
|
"AllowAnyOrigin": false
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "Host=192.168.100.10;Port=5432;Database=fengling_gateway;Username=movingsam;Password=sl52788542"
|
||||||
|
},
|
||||||
|
"Jwt": {
|
||||||
|
"Authority": "https://your-auth-server.com",
|
||||||
|
"Audience": "fengling-gateway",
|
||||||
|
"ValidateIssuer": true,
|
||||||
|
"ValidateAudience": true
|
||||||
|
},
|
||||||
|
"Redis": {
|
||||||
|
"ConnectionString": "192.168.100.10:6379",
|
||||||
|
"Database": 0,
|
||||||
|
"InstanceName": "YarpGateway"
|
||||||
|
},
|
||||||
|
"ReverseProxy": {
|
||||||
|
"Routes": {},
|
||||||
|
"Clusters": {}
|
||||||
|
},
|
||||||
|
"Serilog": {
|
||||||
|
"Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"],
|
||||||
|
"MinimumLevel": "Information",
|
||||||
|
"WriteTo": [
|
||||||
|
{
|
||||||
|
"Name": "Console",
|
||||||
|
"Args": {
|
||||||
|
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "File",
|
||||||
|
"Args": {
|
||||||
|
"path": "logs/gateway-.log",
|
||||||
|
"rollingInterval": "Day",
|
||||||
|
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
|
||||||
|
},
|
||||||
|
"Kestrel": {
|
||||||
|
"Endpoints": {
|
||||||
|
"Http": {
|
||||||
|
"Url": "http://0.0.0.0:8080"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
bin/Debug/net10.0/cs/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll
Executable file
BIN
bin/Debug/net10.0/cs/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/cs/Microsoft.CodeAnalysis.CSharp.resources.dll
Executable file
BIN
bin/Debug/net10.0/cs/Microsoft.CodeAnalysis.CSharp.resources.dll
Executable file
Binary file not shown.
Binary file not shown.
BIN
bin/Debug/net10.0/cs/Microsoft.CodeAnalysis.Workspaces.resources.dll
Executable file
BIN
bin/Debug/net10.0/cs/Microsoft.CodeAnalysis.Workspaces.resources.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/cs/Microsoft.CodeAnalysis.resources.dll
Executable file
BIN
bin/Debug/net10.0/cs/Microsoft.CodeAnalysis.resources.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/de/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll
Executable file
BIN
bin/Debug/net10.0/de/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/de/Microsoft.CodeAnalysis.CSharp.resources.dll
Executable file
BIN
bin/Debug/net10.0/de/Microsoft.CodeAnalysis.CSharp.resources.dll
Executable file
Binary file not shown.
Binary file not shown.
BIN
bin/Debug/net10.0/de/Microsoft.CodeAnalysis.Workspaces.resources.dll
Executable file
BIN
bin/Debug/net10.0/de/Microsoft.CodeAnalysis.Workspaces.resources.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/de/Microsoft.CodeAnalysis.resources.dll
Executable file
BIN
bin/Debug/net10.0/de/Microsoft.CodeAnalysis.resources.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/es/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll
Executable file
BIN
bin/Debug/net10.0/es/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/es/Microsoft.CodeAnalysis.CSharp.resources.dll
Executable file
BIN
bin/Debug/net10.0/es/Microsoft.CodeAnalysis.CSharp.resources.dll
Executable file
Binary file not shown.
Binary file not shown.
BIN
bin/Debug/net10.0/es/Microsoft.CodeAnalysis.Workspaces.resources.dll
Executable file
BIN
bin/Debug/net10.0/es/Microsoft.CodeAnalysis.Workspaces.resources.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/es/Microsoft.CodeAnalysis.resources.dll
Executable file
BIN
bin/Debug/net10.0/es/Microsoft.CodeAnalysis.resources.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/fr/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll
Executable file
BIN
bin/Debug/net10.0/fr/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll
Executable file
Binary file not shown.
BIN
bin/Debug/net10.0/fr/Microsoft.CodeAnalysis.CSharp.resources.dll
Executable file
BIN
bin/Debug/net10.0/fr/Microsoft.CodeAnalysis.CSharp.resources.dll
Executable file
Binary file not shown.
Binary file not shown.
BIN
bin/Debug/net10.0/fr/Microsoft.CodeAnalysis.Workspaces.resources.dll
Executable file
BIN
bin/Debug/net10.0/fr/Microsoft.CodeAnalysis.Workspaces.resources.dll
Executable file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user