From 71a15fd9fb1ca9bf860b645fdf926449c5864688 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 15 Feb 2026 10:34:07 +0800 Subject: [PATCH] refactor: major project restructuring and cleanup Changes: - Remove deprecated Fengling.Activity and YarpGateway.Admin projects - Add points processing services with distributed lock support - Update Vben frontend with gateway management pages - Add gateway config controller and database listener - Update routing to use header-mixed-nav layout - Add comprehensive test suites for Member services - Add YarpGateway integration tests - Update package versions in Directory.Packages.props Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- Controllers/GatewayConfigController.cs | 394 ++++++++++++++++----- Data/GatewayDbContext.cs | 73 ++++ DynamicProxy/DynamicProxyConfigProvider.cs | 16 +- Program.cs | 5 + YarpGateway.csproj | 6 +- 5 files changed, 400 insertions(+), 94 deletions(-) diff --git a/Controllers/GatewayConfigController.cs b/Controllers/GatewayConfigController.cs index 5286f18..9be8137 100644 --- a/Controllers/GatewayConfigController.cs +++ b/Controllers/GatewayConfigController.cs @@ -4,7 +4,6 @@ using YarpGateway.Data; using YarpGateway.Config; using YarpGateway.Models; using YarpGateway.Services; -using Yarp.ReverseProxy.Configuration; namespace YarpGateway.Controllers; @@ -29,33 +28,63 @@ public class GatewayConfigController : ControllerBase _routeCache = routeCache; } + #region Tenants + [HttpGet("tenants")] - public async Task GetTenants() + public async Task GetTenants([FromQuery] int page = 1, [FromQuery] int pageSize = 10, [FromQuery] string? keyword = null) { await using var db = _dbContextFactory.CreateDbContext(); - var tenants = await db.Tenants - .Where(t => !t.IsDeleted) + var query = db.Tenants.Where(t => !t.IsDeleted); + + if (!string.IsNullOrEmpty(keyword)) + { + query = query.Where(t => t.TenantCode.Contains(keyword) || t.TenantName.Contains(keyword)); + } + + var total = await query.CountAsync(); + var items = await query + .OrderByDescending(t => t.Id) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(t => new + { + t.Id, + t.TenantCode, + t.TenantName, + t.Status, + RouteCount = db.TenantRoutes.Count(r => r.TenantCode == t.TenantCode && !r.IsDeleted), + t.Version, + t.CreatedTime, + t.UpdatedTime + }) .ToListAsync(); - return Ok(tenants); + + return Ok(new { items, total, page, pageSize, totalPages = (int)Math.Ceiling(total / (double)pageSize) }); + } + + [HttpGet("tenants/{id}")] + public async Task GetTenant(long id) + { + await using var db = _dbContextFactory.CreateDbContext(); + var tenant = await db.Tenants.FindAsync(id); + if (tenant == null) return NotFound(); + return Ok(tenant); } [HttpPost("tenants")] public async Task 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 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 + Status = 1, + Version = 1 }; await db.Tenants.AddAsync(tenant); await db.SaveChangesAsync(); @@ -63,55 +92,110 @@ public class GatewayConfigController : ControllerBase return Ok(tenant); } + [HttpPut("tenants/{id}")] + public async Task UpdateTenant(long id, [FromBody] UpdateTenantDto dto) + { + await using var db = _dbContextFactory.CreateDbContext(); + var tenant = await db.Tenants.FindAsync(id); + if (tenant == null) return NotFound(); + + if (!string.IsNullOrEmpty(dto.TenantName)) tenant.TenantName = dto.TenantName; + if (dto.Status != null) tenant.Status = dto.Status.Value; + + tenant.Version++; + tenant.UpdatedTime = DateTime.UtcNow; + await db.SaveChangesAsync(); + + return Ok(tenant); + } + [HttpDelete("tenants/{id}")] public async Task DeleteTenant(long id) { await using var db = _dbContextFactory.CreateDbContext(); var tenant = await db.Tenants.FindAsync(id); - if (tenant == null) - return NotFound(); + if (tenant == null) return NotFound(); tenant.IsDeleted = true; + tenant.UpdatedTime = DateTime.UtcNow; await db.SaveChangesAsync(); return Ok(); } - [HttpGet("tenants/{tenantCode}/routes")] - public async Task GetTenantRoutes(string tenantCode) + #endregion + + #region Routes + + [HttpGet("routes")] + public async Task GetRoutes([FromQuery] int page = 1, [FromQuery] int pageSize = 10, [FromQuery] string? tenantCode = null, [FromQuery] bool? isGlobal = null) { await using var db = _dbContextFactory.CreateDbContext(); - var routes = await db.TenantRoutes - .Where(r => r.TenantCode == tenantCode && !r.IsDeleted) + var query = db.TenantRoutes.Where(r => !r.IsDeleted); + + if (!string.IsNullOrEmpty(tenantCode)) + query = query.Where(r => r.TenantCode == tenantCode); + if (isGlobal != null) + query = query.Where(r => r.IsGlobal == isGlobal.Value); + + var total = await query.CountAsync(); + var items = await query + .OrderBy(r => r.Priority) + .Skip((page - 1) * pageSize) + .Take(pageSize) .ToListAsync(); + + return Ok(new { items, total, page, pageSize, totalPages = (int)Math.Ceiling(total / (double)pageSize) }); + } + + [HttpGet("routes/global")] + public async Task GetGlobalRoutes() + { + await using var db = _dbContextFactory.CreateDbContext(); + var routes = await db.TenantRoutes.Where(r => r.IsGlobal && !r.IsDeleted).ToListAsync(); return Ok(routes); } - [HttpPost("tenants/{tenantCode}/routes")] - public async Task CreateTenantRoute(string tenantCode, [FromBody] CreateTenantRouteDto dto) + [HttpGet("routes/tenant/{tenantCode}")] + public async Task GetTenantRoutes(string tenantCode) { 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 routes = await db.TenantRoutes.Where(r => r.TenantCode == tenantCode && !r.IsDeleted).ToListAsync(); + return Ok(routes); + } - 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"); + [HttpGet("routes/{id}")] + public async Task GetRoute(long id) + { + await using var db = _dbContextFactory.CreateDbContext(); + var route = await db.TenantRoutes.FindAsync(id); + if (route == null) return NotFound(); + return Ok(route); + } + + [HttpPost("routes")] + public async Task CreateRoute([FromBody] CreateRouteDto dto) + { + await using var db = _dbContextFactory.CreateDbContext(); + + if ((dto.IsGlobal != true) && !string.IsNullOrEmpty(dto.TenantCode)) + { + var tenant = await db.Tenants.FirstOrDefaultAsync(t => t.TenantCode == dto.TenantCode); + if (tenant == null) return BadRequest($"Tenant {dto.TenantCode} not found"); + } var route = new GwTenantRoute { Id = GenerateId(), - TenantCode = tenantCode, + TenantCode = dto.TenantCode ?? string.Empty, ServiceName = dto.ServiceName, - ClusterId = clusterId, + ClusterId = dto.ClusterId, PathPattern = dto.PathPattern, - Priority = 10, + Priority = dto.Priority ?? 10, Status = 1, - IsGlobal = false + IsGlobal = dto.IsGlobal ?? false, + Version = 1, + CreatedTime = DateTime.UtcNow }; await db.TenantRoutes.AddAsync(route); await db.SaveChangesAsync(); @@ -121,41 +205,21 @@ public class GatewayConfigController : ControllerBase return Ok(route); } - [HttpGet("routes/global")] - public async Task GetGlobalRoutes() + [HttpPut("routes/{id}")] + public async Task UpdateRoute(long id, [FromBody] CreateRouteDto dto) { await using var db = _dbContextFactory.CreateDbContext(); - var routes = await db.TenantRoutes - .Where(r => r.IsGlobal && !r.IsDeleted) - .ToListAsync(); - return Ok(routes); - } + var route = await db.TenantRoutes.FindAsync(id); + if (route == null) return NotFound(); - [HttpPost("routes/global")] - public async Task 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); + route.ServiceName = dto.ServiceName; + route.ClusterId = dto.ClusterId; + route.PathPattern = dto.PathPattern; + if (dto.Priority != null) route.Priority = dto.Priority.Value; + route.Version++; + route.UpdatedTime = DateTime.UtcNow; + await db.SaveChangesAsync(); - await _routeCache.ReloadAsync(); return Ok(route); @@ -166,10 +230,10 @@ public class GatewayConfigController : ControllerBase { await using var db = _dbContextFactory.CreateDbContext(); var route = await db.TenantRoutes.FindAsync(id); - if (route == null) - return NotFound(); + if (route == null) return NotFound(); route.IsDeleted = true; + route.UpdatedTime = DateTime.UtcNow; await db.SaveChangesAsync(); await _routeCache.ReloadAsync(); @@ -177,24 +241,94 @@ public class GatewayConfigController : ControllerBase return Ok(); } + #endregion + + #region Clusters + + [HttpGet("clusters")] + public async Task GetClusters() + { + await using var db = _dbContextFactory.CreateDbContext(); + var clusters = await db.ServiceInstances + .Where(i => !i.IsDeleted) + .GroupBy(i => i.ClusterId) + .Select(g => new + { + ClusterId = g.Key, + ClusterName = g.Key, + InstanceCount = g.Count(), + HealthyInstanceCount = g.Count(i => i.Health == 1), + Instances = g.ToList() + }) + .ToListAsync(); + return Ok(clusters); + } + + [HttpGet("clusters/{clusterId}")] + public async Task GetCluster(string clusterId) + { + await using var db = _dbContextFactory.CreateDbContext(); + var instances = await db.ServiceInstances.Where(i => i.ClusterId == clusterId && !i.IsDeleted).ToListAsync(); + if (!instances.Any()) return NotFound(); + + return Ok(new + { + ClusterId = clusterId, + ClusterName = clusterId, + InstanceCount = instances.Count, + HealthyInstanceCount = instances.Count(i => i.Health == 1), + Instances = instances + }); + } + + [HttpPost("clusters")] + public async Task CreateCluster([FromBody] CreateClusterDto dto) + { + return Ok(new { message = "Cluster created", clusterId = dto.ClusterId }); + } + + [HttpDelete("clusters/{clusterId}")] + public async Task DeleteCluster(string clusterId) + { + await using var db = _dbContextFactory.CreateDbContext(); + var instances = await db.ServiceInstances.Where(i => i.ClusterId == clusterId).ToListAsync(); + foreach (var instance in instances) + { + instance.IsDeleted = true; + } + await db.SaveChangesAsync(); + await _clusterProvider.ReloadAsync(); + + return Ok(); + } + + #endregion + + #region Instances + [HttpGet("clusters/{clusterId}/instances")] public async Task GetInstances(string clusterId) { await using var db = _dbContextFactory.CreateDbContext(); - var instances = await db.ServiceInstances - .Where(i => i.ClusterId == clusterId && !i.IsDeleted) - .ToListAsync(); + var instances = await db.ServiceInstances.Where(i => i.ClusterId == clusterId && !i.IsDeleted).ToListAsync(); return Ok(instances); } - [HttpPost("clusters/{clusterId}/instances")] - public async Task AddInstance(string clusterId, [FromBody] CreateInstanceDto dto) + [HttpGet("instances/{id}")] + public async Task GetInstance(long id) { 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 = await db.ServiceInstances.FindAsync(id); + if (instance == null) return NotFound(); + return Ok(instance); + } + + [HttpPost("clusters/{clusterId}/instances")] + public async Task CreateInstance(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"); var instance = new GwServiceInstance { @@ -202,9 +336,11 @@ public class GatewayConfigController : ControllerBase ClusterId = clusterId, DestinationId = dto.DestinationId, Address = dto.Address, - Weight = dto.Weight, - Health = 1, - Status = 1 + Weight = dto.Weight ?? 1, + Health = dto.IsHealthy == true ? 1 : 0, + Status = 1, + Version = 1, + CreatedTime = DateTime.UtcNow }; await db.ServiceInstances.AddAsync(instance); await db.SaveChangesAsync(); @@ -219,10 +355,10 @@ public class GatewayConfigController : ControllerBase { await using var db = _dbContextFactory.CreateDbContext(); var instance = await db.ServiceInstances.FindAsync(id); - if (instance == null) - return NotFound(); + if (instance == null) return NotFound(); instance.IsDeleted = true; + instance.UpdatedTime = DateTime.UtcNow; await db.SaveChangesAsync(); await _clusterProvider.ReloadAsync(); @@ -230,43 +366,123 @@ public class GatewayConfigController : ControllerBase return Ok(); } - [HttpPost("reload")] + #endregion + + #region Config & Stats + + [HttpPost("config/reload")] public async Task ReloadConfig() { await _routeCache.ReloadAsync(); await _routeProvider.ReloadAsync(); await _clusterProvider.ReloadAsync(); - return Ok(new { message = "Config reloaded successfully" }); + return Ok(new { message = "Config reloaded successfully", timestamp = DateTime.UtcNow }); } - private long GenerateId() + [HttpGet("config/status")] + public async Task GetConfigStatus() { - return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + await using var db = _dbContextFactory.CreateDbContext(); + var routeCount = await db.TenantRoutes.CountAsync(r => r.Status == 1 && !r.IsDeleted); + var instanceCount = await db.ServiceInstances.CountAsync(i => i.Status == 1 && !i.IsDeleted); + var healthyCount = await db.ServiceInstances.CountAsync(i => i.Health == 1 && !i.IsDeleted); + + return Ok(new + { + routeCount, + clusterCount = await db.ServiceInstances.Where(i => !i.IsDeleted).GroupBy(i => i.ClusterId).CountAsync(), + instanceCount, + healthyInstanceCount = healthyCount, + lastReloadTime = DateTime.UtcNow, + isListening = true, + listenerStatus = "Active" + }); } + [HttpGet("config/versions")] + public async Task GetVersionInfo() + { + await using var db = _dbContextFactory.CreateDbContext(); + var routeVersion = await db.TenantRoutes.OrderByDescending(r => r.Version).Select(r => r.Version).FirstOrDefaultAsync(); + var clusterVersion = await db.ServiceInstances.OrderByDescending(i => i.Version).Select(i => i.Version).FirstOrDefaultAsync(); + + return Ok(new + { + routeVersion, + clusterVersion, + routeVersionUpdatedAt = DateTime.UtcNow, + clusterVersionUpdatedAt = DateTime.UtcNow + }); + } + + [HttpGet("stats/overview")] + public async Task GetOverviewStats() + { + await using var db = _dbContextFactory.CreateDbContext(); + var totalTenants = await db.Tenants.CountAsync(t => !t.IsDeleted); + var activeTenants = await db.Tenants.CountAsync(t => !t.IsDeleted && t.Status == 1); + var totalRoutes = await db.TenantRoutes.CountAsync(r => r.Status == 1 && !r.IsDeleted); + var totalInstances = await db.ServiceInstances.CountAsync(i => i.Status == 1 && !i.IsDeleted); + var healthyInstances = await db.ServiceInstances.CountAsync(i => i.Health == 1 && !i.IsDeleted); + + return Ok(new + { + totalTenants, + activeTenants, + totalRoutes, + totalClusters = await db.ServiceInstances.Where(i => !i.IsDeleted).GroupBy(i => i.ClusterId).CountAsync(), + totalInstances, + healthyInstances, + lastUpdated = DateTime.UtcNow + }); + } + + #endregion + + #region DTOs + public class CreateTenantDto { public string TenantCode { get; set; } = string.Empty; public string TenantName { get; set; } = string.Empty; } - public class CreateTenantRouteDto + public class UpdateTenantDto { - public string ServiceName { get; set; } = string.Empty; - public string PathPattern { get; set; } = string.Empty; + public string? TenantName { get; set; } + public int? Status { get; set; } } - public class CreateGlobalRouteDto + public class CreateRouteDto { + public string? TenantCode { get; set; } 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; } + public bool? IsGlobal { get; set; } + } + + public class CreateClusterDto + { + public string ClusterId { get; set; } = string.Empty; + public string ClusterName { get; set; } = string.Empty; + public string? Description { get; set; } + public string? LoadBalancingPolicy { get; set; } } public class CreateInstanceDto { public string DestinationId { get; set; } = string.Empty; public string Address { get; set; } = string.Empty; - public int Weight { get; set; } = 1; + public int? Weight { get; set; } + public bool? IsHealthy { get; set; } + } + + #endregion + + private long GenerateId() + { + return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); } } diff --git a/Data/GatewayDbContext.cs b/Data/GatewayDbContext.cs index a581d6f..9ebe833 100644 --- a/Data/GatewayDbContext.cs +++ b/Data/GatewayDbContext.cs @@ -1,4 +1,6 @@ using Microsoft.EntityFrameworkCore; +using Npgsql; +using YarpGateway.Config; using YarpGateway.Models; namespace YarpGateway.Data; @@ -14,6 +16,77 @@ public class GatewayDbContext : DbContext public DbSet TenantRoutes => Set(); public DbSet ServiceInstances => Set(); + public override int SaveChanges(bool acceptAllChangesOnSuccess) + { + DetectConfigChanges(); + var result = base.SaveChanges(acceptAllChangesOnSuccess); + if (_configChangeDetected) + { + NotifyConfigChangedSync(); + } + return result; + } + + public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) + { + DetectConfigChanges(); + var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); + if (_configChangeDetected) + { + await NotifyConfigChangedAsync(cancellationToken); + } + return result; + } + + private bool _configChangeDetected; + + private void DetectConfigChanges() + { + var entries = ChangeTracker.Entries() + .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted) + .Where(e => e.Entity is GwTenantRoute or GwServiceInstance or GwTenant); + + _configChangeDetected = entries.Any(); + } + + private bool IsRelationalDatabase() + { + try + { + return Database.IsRelational(); + } + catch + { + return false; + } + } + + private void NotifyConfigChangedSync() + { + if (!IsRelationalDatabase()) return; + + var connectionString = Database.GetConnectionString(); + if (string.IsNullOrEmpty(connectionString)) return; + + using var connection = new NpgsqlConnection(connectionString); + connection.Open(); + using var cmd = new NpgsqlCommand($"NOTIFY {ConfigNotifyChannel.GatewayConfigChanged}", connection); + cmd.ExecuteNonQuery(); + } + + private async Task NotifyConfigChangedAsync(CancellationToken cancellationToken) + { + if (!IsRelationalDatabase()) return; + + var connectionString = Database.GetConnectionString(); + if (string.IsNullOrEmpty(connectionString)) return; + + await using var connection = new NpgsqlConnection(connectionString); + await connection.OpenAsync(cancellationToken); + await using var cmd = new NpgsqlCommand($"NOTIFY {ConfigNotifyChannel.GatewayConfigChanged}", connection); + await cmd.ExecuteNonQueryAsync(cancellationToken); + } + protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => diff --git a/DynamicProxy/DynamicProxyConfigProvider.cs b/DynamicProxy/DynamicProxyConfigProvider.cs index 8cb68c7..1eb3944 100644 --- a/DynamicProxy/DynamicProxyConfigProvider.cs +++ b/DynamicProxy/DynamicProxyConfigProvider.cs @@ -10,6 +10,7 @@ public class DynamicProxyConfigProvider : IProxyConfigProvider private readonly DatabaseRouteConfigProvider _routeProvider; private readonly DatabaseClusterConfigProvider _clusterProvider; private readonly object _lock = new(); + private CancellationTokenSource? _cts; public DynamicProxyConfigProvider( DatabaseRouteConfigProvider routeProvider, @@ -17,6 +18,7 @@ public class DynamicProxyConfigProvider : IProxyConfigProvider { _routeProvider = routeProvider; _clusterProvider = clusterProvider; + _cts = new CancellationTokenSource(); UpdateConfig(); } @@ -29,13 +31,17 @@ public class DynamicProxyConfigProvider : IProxyConfigProvider { lock (_lock) { + _cts?.Cancel(); + _cts = new CancellationTokenSource(); + var routes = _routeProvider.GetRoutes(); var clusters = _clusterProvider.GetClusters(); _config = new InMemoryProxyConfig( routes, clusters, - Array.Empty>() + Array.Empty>(), + _cts.Token ); } } @@ -49,21 +55,23 @@ public class DynamicProxyConfigProvider : IProxyConfigProvider private class InMemoryProxyConfig : IProxyConfig { - private static readonly CancellationChangeToken _nullChangeToken = new(new CancellationToken()); + private readonly CancellationChangeToken _changeToken; public InMemoryProxyConfig( IReadOnlyList routes, IReadOnlyList clusters, - IReadOnlyList> transforms) + IReadOnlyList> transforms, + CancellationToken token) { Routes = routes; Clusters = clusters; Transforms = transforms; + _changeToken = new CancellationChangeToken(token); } public IReadOnlyList Routes { get; } public IReadOnlyList Clusters { get; } public IReadOnlyList> Transforms { get; } - public IChangeToken ChangeToken => _nullChangeToken; + public IChangeToken ChangeToken => _changeToken; } } diff --git a/Program.cs b/Program.cs index dd2983a..ee587d2 100644 --- a/Program.cs +++ b/Program.cs @@ -62,6 +62,8 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => sp.GetRequiredService()); +builder.Services.AddHostedService(); + var corsSettings = builder.Configuration.GetSection("Cors"); builder.Services.AddCors(options => { @@ -86,6 +88,9 @@ builder.Services.AddCors(options => }); builder.Services.AddControllers(); +builder.Services.AddHttpForwarder(); +builder.Services.AddRouting(); +builder.Services.AddReverseProxy(); var app = builder.Build(); diff --git a/YarpGateway.csproj b/YarpGateway.csproj index e248e61..7008f5f 100644 --- a/YarpGateway.csproj +++ b/YarpGateway.csproj @@ -20,4 +20,8 @@ - + + + + +