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