diff --git a/samples/ReverseProxy.Config.Sample/Startup.cs b/samples/ReverseProxy.Config.Sample/Startup.cs
index c8b45cbe76..135e8f5f2f 100644
--- a/samples/ReverseProxy.Config.Sample/Startup.cs
+++ b/samples/ReverseProxy.Config.Sample/Startup.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.ReverseProxy.Middleware;
@@ -63,7 +64,9 @@ public void Configure(IApplicationBuilder app)
proxyPipeline.UseAffinitizedDestinationLookup();
proxyPipeline.UseProxyLoadBalancing();
proxyPipeline.UseRequestAffinitizer();
- });
+ })
+ .ConfigureRoutes((builder, route) => builder.WithDisplayName($"ReverseProxy {route.RouteId}-{route.ClusterId}"))
+ .ConfigureRoute("route1", builder => builder.WithDisplayName("ReverseProxy (My special route name)"));
});
}
}
diff --git a/src/ReverseProxy/Abstractions/RouteDiscovery/IRuntimeRouteBuilder.cs b/src/ReverseProxy/Abstractions/RouteDiscovery/IRuntimeRouteBuilder.cs
index d346eb58d3..3da5ca11b5 100644
--- a/src/ReverseProxy/Abstractions/RouteDiscovery/IRuntimeRouteBuilder.cs
+++ b/src/ReverseProxy/Abstractions/RouteDiscovery/IRuntimeRouteBuilder.cs
@@ -29,10 +29,5 @@ internal interface IRuntimeRouteBuilder
/// and .
///
RouteConfig Build(ProxyRoute source, ClusterInfo cluster, RouteInfo runtimeRoute);
-
- ///
- /// Sets the middleware pipeline to use when building routes.
- ///
- void SetProxyPipeline(RequestDelegate pipeline);
}
}
diff --git a/src/ReverseProxy/Configuration/ConfigurationConfigProvider.cs b/src/ReverseProxy/Configuration/ConfigurationConfigProvider.cs
index f6c6f3fa26..e1a32b3285 100644
--- a/src/ReverseProxy/Configuration/ConfigurationConfigProvider.cs
+++ b/src/ReverseProxy/Configuration/ConfigurationConfigProvider.cs
@@ -11,6 +11,7 @@
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.ReverseProxy.Abstractions;
+using Microsoft.ReverseProxy.Abstractions.ClusterDiscovery.Contract;
using Microsoft.ReverseProxy.Configuration.Contract;
using Microsoft.ReverseProxy.Service;
diff --git a/src/ReverseProxy/Configuration/DependencyInjection/ReverseProxyServiceCollectionExtensions.cs b/src/ReverseProxy/Configuration/DependencyInjection/ReverseProxyServiceCollectionExtensions.cs
index e52ee2e063..5cf77741d8 100644
--- a/src/ReverseProxy/Configuration/DependencyInjection/ReverseProxyServiceCollectionExtensions.cs
+++ b/src/ReverseProxy/Configuration/DependencyInjection/ReverseProxyServiceCollectionExtensions.cs
@@ -3,6 +3,8 @@
using System;
using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.ReverseProxy;
using Microsoft.ReverseProxy.Configuration;
using Microsoft.ReverseProxy.Configuration.Contract;
using Microsoft.ReverseProxy.Configuration.DependencyInjection;
@@ -32,6 +34,8 @@ public static IReverseProxyBuilder AddReverseProxy(this IServiceCollection servi
.AddProxy()
.AddBackgroundWorkers();
+ services.TryAddSingleton();
+
services.AddDataProtection();
services.AddAuthorization();
services.AddCors();
diff --git a/src/ReverseProxy/Configuration/ReverseProxyIEndpointRouteBuilderExtensions.cs b/src/ReverseProxy/Configuration/ReverseProxyIEndpointRouteBuilderExtensions.cs
index 4970d23d69..712a49c996 100644
--- a/src/ReverseProxy/Configuration/ReverseProxyIEndpointRouteBuilderExtensions.cs
+++ b/src/ReverseProxy/Configuration/ReverseProxyIEndpointRouteBuilderExtensions.cs
@@ -2,11 +2,14 @@
// Licensed under the MIT License.
using System;
+using System.Linq;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.ReverseProxy;
using Microsoft.ReverseProxy.Abstractions;
using Microsoft.ReverseProxy.Middleware;
using Microsoft.ReverseProxy.Service;
+using Microsoft.ReverseProxy.Service.Management;
namespace Microsoft.AspNetCore.Builder
{
@@ -33,7 +36,7 @@ public static void MapReverseProxy(this IEndpointRouteBuilder endpoints)
/// Adds Reverse Proxy routes to the route table with the customized processing pipeline. The pipeline includes
/// by default the initialization step and the final proxy step, but not LoadBalancingMiddleware or other intermediate components.
///
- public static void MapReverseProxy(this IEndpointRouteBuilder endpoints, Action configureApp)
+ public static ReverseProxyConventionBuilder MapReverseProxy(this IEndpointRouteBuilder endpoints, Action configureApp)
{
if (endpoints is null)
{
@@ -50,16 +53,27 @@ public static void MapReverseProxy(this IEndpointRouteBuilder endpoints, Action<
appBuilder.UseMiddleware();
var app = appBuilder.Build();
- var routeBuilder = endpoints.ServiceProvider.GetRequiredService();
- routeBuilder.SetProxyPipeline(app);
+ var proxyEndpointFactory = endpoints.ServiceProvider.GetRequiredService();
+ proxyEndpointFactory.SetProxyPipeline(app);
- var configManager = endpoints.ServiceProvider.GetRequiredService();
+ return GetOrCreateDataSource(endpoints).DefaultBuilder;
+ }
+
+ private static ProxyConfigManager GetOrCreateDataSource(IEndpointRouteBuilder endpoints)
+ {
+ var dataSource = (ProxyConfigManager)endpoints.DataSources.OfType().FirstOrDefault();
+ if (dataSource == null)
+ {
+ dataSource = (ProxyConfigManager)endpoints.ServiceProvider.GetRequiredService();
+ endpoints.DataSources.Add(dataSource);
+
+ // Config validation is async but startup is sync. We want this to block so that A) any validation errors can prevent
+ // the app from starting, and B) so that all the config is ready before the server starts accepting requests.
+ // Reloads will be async.
+ dataSource.InitialLoadAsync().GetAwaiter().GetResult();
+ }
- // Config validation is async but startup is sync. We want this to block so that A) any validation errors can prevent
- // the app from starting, and B) so that all the config is ready before the server starts accepting requests.
- // Reloads will be async.
- var dataSource = configManager.InitialLoadAsync().GetAwaiter().GetResult();
- endpoints.DataSources.Add(dataSource);
+ return dataSource;
}
}
}
diff --git a/src/ReverseProxy/Service/Config/RuntimeRouteBuilder.cs b/src/ReverseProxy/Service/Config/RuntimeRouteBuilder.cs
index 22a051b366..c1870325c4 100644
--- a/src/ReverseProxy/Service/Config/RuntimeRouteBuilder.cs
+++ b/src/ReverseProxy/Service/Config/RuntimeRouteBuilder.cs
@@ -21,23 +21,13 @@ namespace Microsoft.ReverseProxy.Service
///
internal class RuntimeRouteBuilder : IRuntimeRouteBuilder
{
- private static readonly IAuthorizeData DefaultAuthorization = new AuthorizeAttribute();
- private static readonly IEnableCorsAttribute DefaultCors = new EnableCorsAttribute();
- private static readonly IDisableCorsAttribute DisableCors = new DisableCorsAttribute();
-
private readonly ITransformBuilder _transformBuilder;
- private RequestDelegate _pipeline;
public RuntimeRouteBuilder(ITransformBuilder transformBuilder)
{
_transformBuilder = transformBuilder ?? throw new ArgumentNullException(nameof(transformBuilder));
}
- public void SetProxyPipeline(RequestDelegate pipeline)
- {
- _pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline));
- }
-
///
public RouteConfig Build(ProxyRoute source, ClusterInfo cluster, RouteInfo runtimeRoute)
{
@@ -46,74 +36,12 @@ public RouteConfig Build(ProxyRoute source, ClusterInfo cluster, RouteInfo runti
var transforms = _transformBuilder.Build(source.Transforms);
- // NOTE: `new RouteConfig(...)` needs a reference to the list of ASP .NET Core endpoints,
- // but the ASP .NET Core endpoints cannot be created without a `RouteConfig` metadata item.
- // We solve this chicken-egg problem by creating an (empty) list first
- // and passing a read-only wrapper of it to `RouteConfig.ctor`.
- // Recall that `List.AsReadOnly()` creates a wrapper over the original list,
- // and changes to the underlying list *are* reflected on the read-only view.
- var aspNetCoreEndpoints = new List(1);
var newRouteConfig = new RouteConfig(
runtimeRoute,
source,
cluster,
- aspNetCoreEndpoints.AsReadOnly(),
transforms);
- // Catch-all pattern when no path was specified
- var pathPattern = string.IsNullOrEmpty(source.Match.Path) ? "/{**catchall}" : source.Match.Path;
-
- var endpointBuilder = new AspNetCore.Routing.RouteEndpointBuilder(
- requestDelegate: _pipeline,
- routePattern: AspNetCore.Routing.Patterns.RoutePatternFactory.Parse(pathPattern),
- order: source.Order.GetValueOrDefault());
-
- endpointBuilder.DisplayName = source.RouteId;
- endpointBuilder.Metadata.Add(newRouteConfig);
-
- if (source.Match.Hosts != null && source.Match.Hosts.Count != 0)
- {
- endpointBuilder.Metadata.Add(new AspNetCore.Routing.HostAttribute(source.Match.Hosts.ToArray()));
- }
-
- bool acceptCorsPreflight;
- if (string.Equals(CorsConstants.Default, source.CorsPolicy, StringComparison.OrdinalIgnoreCase))
- {
- endpointBuilder.Metadata.Add(DefaultCors);
- acceptCorsPreflight = true;
- }
- else if (string.Equals(CorsConstants.Disable, source.CorsPolicy, StringComparison.OrdinalIgnoreCase))
- {
- endpointBuilder.Metadata.Add(DisableCors);
- acceptCorsPreflight = true;
- }
- else if (!string.IsNullOrEmpty(source.CorsPolicy))
- {
- endpointBuilder.Metadata.Add(new EnableCorsAttribute(source.CorsPolicy));
- acceptCorsPreflight = true;
- }
- else
- {
- acceptCorsPreflight = false;
- }
-
- if (source.Match.Methods != null && source.Match.Methods.Count > 0)
- {
- endpointBuilder.Metadata.Add(new AspNetCore.Routing.HttpMethodMetadata(source.Match.Methods, acceptCorsPreflight));
- }
-
- if (string.Equals(AuthorizationConstants.Default, source.AuthorizationPolicy, StringComparison.OrdinalIgnoreCase))
- {
- endpointBuilder.Metadata.Add(DefaultAuthorization);
- }
- else if (!string.IsNullOrEmpty(source.AuthorizationPolicy))
- {
- endpointBuilder.Metadata.Add(new AuthorizeAttribute(source.AuthorizationPolicy));
- }
-
- var endpoint = endpointBuilder.Build();
- aspNetCoreEndpoints.Add(endpoint);
-
return newRouteConfig;
}
}
diff --git a/src/ReverseProxy/Service/DynamicEndpoint/ProxyEndpointFactory.cs b/src/ReverseProxy/Service/DynamicEndpoint/ProxyEndpointFactory.cs
new file mode 100644
index 0000000000..176d708c16
--- /dev/null
+++ b/src/ReverseProxy/Service/DynamicEndpoint/ProxyEndpointFactory.cs
@@ -0,0 +1,109 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Cors;
+using Microsoft.AspNetCore.Cors.Infrastructure;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.ReverseProxy.Abstractions.RouteDiscovery.Contract;
+using Microsoft.ReverseProxy.RuntimeModel;
+using CorsConstants = Microsoft.ReverseProxy.Abstractions.RouteDiscovery.Contract.CorsConstants;
+
+namespace Microsoft.ReverseProxy
+{
+ internal class ProxyEndpointFactory
+ {
+ private static readonly IAuthorizeData _defaultAuthorization = new AuthorizeAttribute();
+ private static readonly IEnableCorsAttribute _defaultCors = new EnableCorsAttribute();
+ private static readonly IDisableCorsAttribute _disableCors = new DisableCorsAttribute();
+
+ private RequestDelegate _pipeline;
+
+ public void AddEndpoint(List endpoints,
+ RouteConfig route,
+ IReadOnlyList> conventions)
+ {
+ var proxyRoute = route.ProxyRoute;
+ var proxyMatch = proxyRoute.Match;
+
+ // Catch-all pattern when no path was specified
+ var pathPattern = string.IsNullOrEmpty(proxyMatch.Path) ? "/{**catchall}" : proxyMatch.Path;
+
+ var endpointBuilder = new AspNetCore.Routing.RouteEndpointBuilder(
+ requestDelegate: _pipeline ?? Invoke,
+ routePattern: AspNetCore.Routing.Patterns.RoutePatternFactory.Parse(pathPattern),
+ order: proxyRoute.Order.GetValueOrDefault())
+ {
+ DisplayName = proxyRoute.RouteId
+ };
+
+ endpointBuilder.Metadata.Add(route);
+
+ if (proxyMatch.Hosts != null && proxyMatch.Hosts.Count != 0)
+ {
+ endpointBuilder.Metadata.Add(new HostAttribute(proxyMatch.Hosts.ToArray()));
+ }
+
+ bool acceptCorsPreflight;
+ if (string.Equals(CorsConstants.Default, proxyRoute.CorsPolicy, StringComparison.OrdinalIgnoreCase))
+ {
+ endpointBuilder.Metadata.Add(_defaultCors);
+ acceptCorsPreflight = true;
+ }
+ else if (string.Equals(CorsConstants.Disable, proxyRoute.CorsPolicy, StringComparison.OrdinalIgnoreCase))
+ {
+ endpointBuilder.Metadata.Add(_disableCors);
+ acceptCorsPreflight = true;
+ }
+ else if (!string.IsNullOrEmpty(proxyRoute.CorsPolicy))
+ {
+ endpointBuilder.Metadata.Add(new EnableCorsAttribute(proxyRoute.CorsPolicy));
+ acceptCorsPreflight = true;
+ }
+ else
+ {
+ acceptCorsPreflight = false;
+ }
+
+ if (proxyMatch.Methods != null && proxyMatch.Methods.Count > 0)
+ {
+ endpointBuilder.Metadata.Add(new HttpMethodMetadata(proxyMatch.Methods, acceptCorsPreflight));
+ }
+
+ if (string.Equals(AuthorizationConstants.Default, proxyRoute.AuthorizationPolicy, StringComparison.OrdinalIgnoreCase))
+ {
+ endpointBuilder.Metadata.Add(_defaultAuthorization);
+ }
+ else if (!string.IsNullOrEmpty(proxyRoute.AuthorizationPolicy))
+ {
+ endpointBuilder.Metadata.Add(new AuthorizeAttribute(proxyRoute.AuthorizationPolicy));
+ }
+
+ for (var i = 0; i < conventions.Count; i++)
+ {
+ conventions[i](endpointBuilder);
+ }
+
+ endpoints.Add(endpointBuilder.Build());
+ }
+
+ // This indirection is needed because on startup the routes are loaded from config and built before the
+ // proxy pipeline gets built.
+ private Task Invoke(HttpContext context)
+ {
+ var pipeline = _pipeline ?? throw new InvalidOperationException("The pipeline hasn't been provided yet.");
+ return pipeline(context);
+ }
+
+ public void SetProxyPipeline(RequestDelegate pipeline)
+ {
+ _pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline));
+ }
+ }
+}
diff --git a/src/ReverseProxy/Service/DynamicEndpoint/ReverseProxyConventionBuilder.cs b/src/ReverseProxy/Service/DynamicEndpoint/ReverseProxyConventionBuilder.cs
new file mode 100644
index 0000000000..5586ad2602
--- /dev/null
+++ b/src/ReverseProxy/Service/DynamicEndpoint/ReverseProxyConventionBuilder.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.ReverseProxy.Abstractions;
+using Microsoft.ReverseProxy.RuntimeModel;
+using Microsoft.ReverseProxy.Service;
+
+namespace Microsoft.ReverseProxy
+{
+ public class ReverseProxyConventionBuilder : IEndpointConventionBuilder
+ {
+ // The lock is shared with the data source.
+ private readonly object _lock;
+ private readonly List> _conventions;
+ private readonly IProxyConfigProvider _proxyConfigProvider;
+
+ internal ReverseProxyConventionBuilder(object @lock, List> conventions, IProxyConfigProvider proxyConfigProvider)
+ {
+ _lock = @lock;
+ _conventions = conventions;
+ _proxyConfigProvider = proxyConfigProvider;
+ }
+
+ ///
+ /// Adds the specified convention to the builder. Conventions are used to customize instances.
+ ///
+ /// The convention to add to the builder.
+ public void Add(Action convention)
+ {
+ if (convention == null)
+ {
+ throw new ArgumentNullException(nameof(convention));
+ }
+
+ // The lock is shared with the data source. We want to lock here
+ // to avoid mutating this list while its read in the data source.
+ lock (_lock)
+ {
+ _conventions.Add(convention);
+ }
+ }
+
+ public ReverseProxyConventionBuilder ConfigureClusters(Action convention)
+ {
+ void Action(EndpointBuilder endpointBuilder)
+ {
+ var routeConfig = endpointBuilder.Metadata.OfType().Single();
+
+ var config = _proxyConfigProvider.GetConfig();
+ var cluster = config.Clusters.Single(x => x.Id.Equals(routeConfig.Cluster.ClusterId, StringComparison.OrdinalIgnoreCase));
+
+ var conventionBuilder = new EndpointBuilderConventionBuilder(endpointBuilder);
+ convention(conventionBuilder, cluster.DeepClone());
+ }
+
+ Add(Action);
+
+ return this;
+ }
+
+ public ReverseProxyConventionBuilder ConfigureRoutes(Action convention)
+ {
+ void Action(EndpointBuilder endpointBuilder)
+ {
+ var routeConfig = endpointBuilder.Metadata.OfType().Single();
+
+ var config = _proxyConfigProvider.GetConfig();
+ var route = config.Routes.Single(x => x.RouteId.Equals(routeConfig.Cluster.ClusterId, StringComparison.OrdinalIgnoreCase));
+
+ var conventionBuilder = new EndpointBuilderConventionBuilder(endpointBuilder);
+ convention(conventionBuilder, route.DeepClone());
+ }
+
+ Add(Action);
+
+ return this;
+ }
+
+ public ReverseProxyConventionBuilder ConfigureCluster(string clusterId, Action convention)
+ {
+ void Action(EndpointBuilder endpointBuilder)
+ {
+ var routeConfig = endpointBuilder.Metadata.OfType().Single();
+ if (routeConfig.Cluster.ClusterId.Equals(clusterId, StringComparison.OrdinalIgnoreCase))
+ {
+ var conventionBuilder = new EndpointBuilderConventionBuilder(endpointBuilder);
+ convention(conventionBuilder);
+ }
+ }
+
+ Add(Action);
+
+ return this;
+ }
+
+ public ReverseProxyConventionBuilder ConfigureRoute(string routeId, Action convention)
+ {
+ void Action(EndpointBuilder endpointBuilder)
+ {
+ var routeConfig = endpointBuilder.Metadata.OfType().Single();
+ if (routeConfig.Route.RouteId.Equals(routeId, StringComparison.OrdinalIgnoreCase))
+ {
+ var conventionBuilder = new EndpointBuilderConventionBuilder(endpointBuilder);
+ convention(conventionBuilder);
+ }
+ }
+
+ Add(Action);
+
+ return this;
+ }
+
+ internal class EndpointBuilderConventionBuilder : IEndpointConventionBuilder
+ {
+ private readonly EndpointBuilder _endpointBuilder;
+
+ public EndpointBuilderConventionBuilder(EndpointBuilder endpointBuilder)
+ {
+ _endpointBuilder = endpointBuilder;
+ }
+
+ public void Add(Action convention)
+ {
+ convention(_endpointBuilder);
+ }
+ }
+
+ }
+}
diff --git a/src/ReverseProxy/Service/Management/ProxyConfigManager.cs b/src/ReverseProxy/Service/Management/ProxyConfigManager.cs
index 79631db271..e233929202 100644
--- a/src/ReverseProxy/Service/Management/ProxyConfigManager.cs
+++ b/src/ReverseProxy/Service/Management/ProxyConfigManager.cs
@@ -4,10 +4,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Net.Security;
-using System.Security.Authentication;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
@@ -39,9 +38,11 @@ internal class ProxyConfigManager : EndpointDataSource, IProxyConfigManager, IDi
private readonly IEnumerable _filters;
private readonly IConfigValidator _configValidator;
private readonly IProxyHttpClientFactory _httpClientFactory;
+ private readonly ProxyEndpointFactory _proxyEndpointFactory;
+ private readonly List> _conventions;
private IDisposable _changeSubscription;
- private List _endpoints = new List(0);
+ private List _endpoints;
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private IChangeToken _changeToken;
@@ -53,6 +54,7 @@ public ProxyConfigManager(
IRouteManager routeManager,
IEnumerable filters,
IConfigValidator configValidator,
+ ProxyEndpointFactory proxyEndpointFactory,
IProxyHttpClientFactory httpClientFactory)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -62,15 +64,53 @@ public ProxyConfigManager(
_routeManager = routeManager ?? throw new ArgumentNullException(nameof(routeManager));
_filters = filters ?? throw new ArgumentNullException(nameof(filters));
_configValidator = configValidator ?? throw new ArgumentNullException(nameof(configValidator));
+ _proxyEndpointFactory = proxyEndpointFactory;
+ _conventions = new List>();
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
+ DefaultBuilder = new ReverseProxyConventionBuilder(_syncRoot, _conventions, provider);
_changeToken = new CancellationChangeToken(_cancellationTokenSource.Token);
}
+ public ReverseProxyConventionBuilder DefaultBuilder { get; }
+
// EndpointDataSource
///
- public override IReadOnlyList Endpoints => Volatile.Read(ref _endpoints);
+ public override IReadOnlyList Endpoints
+ {
+ get
+ {
+ Initialize();
+ return _endpoints;
+ }
+ }
+
+ private void Initialize()
+ {
+ if (_endpoints == null)
+ {
+ lock (_syncRoot)
+ {
+ if (_endpoints == null)
+ {
+ CreateEndpoints();
+ }
+ }
+ }
+ }
+
+ private void CreateEndpoints()
+ {
+ var endpoints = new List();
+ foreach (var existingRoute in _routeManager.GetItems())
+ {
+ var runtimeConfig = existingRoute.Config.Value;
+ _proxyEndpointFactory.AddEndpoint(endpoints, runtimeConfig, _conventions);
+ }
+
+ UpdateEndpoints(endpoints);
+ }
///
public override IChangeToken GetChangeToken() => Volatile.Read(ref _changeToken);
@@ -124,7 +164,11 @@ private async Task ReloadConfigAsync()
try
{
- await ApplyConfigAsync(newConfig);
+ var hasChanged = await ApplyConfigAsync(newConfig);
+ if (hasChanged)
+ {
+ CreateEndpoints();
+ }
}
catch (Exception ex)
{
@@ -138,7 +182,7 @@ private async Task ReloadConfigAsync()
}
// Throws for validation failures
- private async Task ApplyConfigAsync(IProxyConfig config)
+ private async Task ApplyConfigAsync(IProxyConfig config)
{
var (configuredRoutes, routeErrors) = await VerifyRoutesAsync(config.Routes, cancellation: default);
var (configuredClusters, clusterErrors) = await VerifyClustersAsync(config.Clusters, cancellation: default);
@@ -150,7 +194,8 @@ private async Task ApplyConfigAsync(IProxyConfig config)
// Update clusters first because routes need to reference them.
UpdateRuntimeClusters(configuredClusters);
- UpdateRuntimeRoutes(configuredRoutes);
+ var hasChanged = UpdateRuntimeRoutes(configuredRoutes);
+ return hasChanged;
}
private async Task<(IList, IList)> VerifyRoutesAsync(IReadOnlyList routes, CancellationToken cancellation)
@@ -380,7 +425,7 @@ private void UpdateRuntimeDestinations(IDictionary newDesti
}
}
- private void UpdateRuntimeRoutes(IList routes)
+ private bool UpdateRuntimeRoutes(IList routes)
{
var desiredRoutes = new HashSet(StringComparer.OrdinalIgnoreCase);
var changed = false;
@@ -435,20 +480,7 @@ private void UpdateRuntimeRoutes(IList routes)
}
}
- if (changed)
- {
- var endpoints = new List();
- foreach (var existingRoute in _routeManager.GetItems())
- {
- var runtimeConfig = existingRoute.Config.Value;
- if (runtimeConfig?.Endpoints != null)
- {
- endpoints.AddRange(runtimeConfig.Endpoints);
- }
- }
-
- UpdateEndpoints(endpoints);
- }
+ return changed;
}
///
diff --git a/src/ReverseProxy/Service/RuntimeModel/RouteConfig.cs b/src/ReverseProxy/Service/RuntimeModel/RouteConfig.cs
index 3c48b0deea..4489a64f35 100644
--- a/src/ReverseProxy/Service/RuntimeModel/RouteConfig.cs
+++ b/src/ReverseProxy/Service/RuntimeModel/RouteConfig.cs
@@ -20,19 +20,15 @@ namespace Microsoft.ReverseProxy.RuntimeModel
///
internal sealed class RouteConfig
{
- private readonly ProxyRoute _proxyRoute;
-
public RouteConfig(
RouteInfo route,
ProxyRoute proxyRoute,
ClusterInfo cluster,
- IReadOnlyList aspNetCoreEndpoints,
Transforms transforms)
{
Route = route ?? throw new ArgumentNullException(nameof(route));
- Endpoints = aspNetCoreEndpoints ?? throw new ArgumentNullException(nameof(aspNetCoreEndpoints));
- _proxyRoute = proxyRoute;
+ ProxyRoute = proxyRoute;
Order = proxyRoute.Order;
Cluster = cluster;
Transforms = transforms;
@@ -45,14 +41,14 @@ public RouteConfig(
// May not be populated if the cluster config is missing.
public ClusterInfo Cluster { get; }
- public IReadOnlyList Endpoints { get; }
-
public Transforms Transforms { get; }
+ internal ProxyRoute ProxyRoute { get; }
+
public bool HasConfigChanged(ProxyRoute newConfig, ClusterInfo cluster)
{
return Cluster != cluster
- || !ProxyRoute.Equals(_proxyRoute, newConfig);
+ || !ProxyRoute.Equals(ProxyRoute, newConfig);
}
}
}
diff --git a/test/ReverseProxy.Tests/Middleware/AffinityMiddlewareTestBase.cs b/test/ReverseProxy.Tests/Middleware/AffinityMiddlewareTestBase.cs
index 05ba59400a..b04fc006f9 100644
--- a/test/ReverseProxy.Tests/Middleware/AffinityMiddlewareTestBase.cs
+++ b/test/ReverseProxy.Tests/Middleware/AffinityMiddlewareTestBase.cs
@@ -91,11 +91,9 @@ internal IReverseProxyFeature GetDestinationsFeature(IReadOnlyList(1);
var proxyRoute = new ProxyRoute();
- var routeConfig = new RouteConfig(new RouteInfo("route-1"), proxyRoute, cluster, endpoints.AsReadOnly(), Transforms.Empty);
+ var routeConfig = new RouteConfig(new RouteInfo("route-1"), proxyRoute, cluster, Transforms.Empty);
var endpoint = new Endpoint(default, new EndpointMetadataCollection(routeConfig), string.Empty);
- endpoints.Add(endpoint);
return endpoint;
}
}
diff --git a/test/ReverseProxy.Tests/Middleware/DestinationInitializerMiddlewareTests.cs b/test/ReverseProxy.Tests/Middleware/DestinationInitializerMiddlewareTests.cs
index 980ae15d8e..35a9f0eddf 100644
--- a/test/ReverseProxy.Tests/Middleware/DestinationInitializerMiddlewareTests.cs
+++ b/test/ReverseProxy.Tests/Middleware/DestinationInitializerMiddlewareTests.cs
@@ -51,7 +51,6 @@ public async Task Invoke_SetsFeatures()
new RouteInfo("route1"),
proxyRoute: new ProxyRoute(),
cluster1,
- aspNetCoreEndpoints.AsReadOnly(),
transforms: null);
var aspNetCoreEndpoint = CreateAspNetCoreEndpoint(routeConfig);
aspNetCoreEndpoints.Add(aspNetCoreEndpoint);
@@ -99,7 +98,6 @@ public async Task Invoke_NoHealthyEndpoints_503()
route: new RouteInfo("route1"),
proxyRoute: new ProxyRoute(),
cluster: cluster1,
- aspNetCoreEndpoints: aspNetCoreEndpoints.AsReadOnly(),
transforms: null);
var aspNetCoreEndpoint = CreateAspNetCoreEndpoint(routeConfig);
aspNetCoreEndpoints.Add(aspNetCoreEndpoint);
diff --git a/test/ReverseProxy.Tests/Middleware/LoadBalancerMiddlewareTests.cs b/test/ReverseProxy.Tests/Middleware/LoadBalancerMiddlewareTests.cs
index c6721690d6..6df3b8f0b4 100644
--- a/test/ReverseProxy.Tests/Middleware/LoadBalancerMiddlewareTests.cs
+++ b/test/ReverseProxy.Tests/Middleware/LoadBalancerMiddlewareTests.cs
@@ -61,7 +61,6 @@ public async Task Invoke_Works()
route: new RouteInfo("route1"),
proxyRoute: new ProxyRoute(),
cluster: cluster1,
- aspNetCoreEndpoints: aspNetCoreEndpoints.AsReadOnly(),
transforms: null);
var aspNetCoreEndpoint = CreateAspNetCoreEndpoint(routeConfig);
aspNetCoreEndpoints.Add(aspNetCoreEndpoint);
@@ -118,7 +117,6 @@ public async Task Invoke_ServiceReturnsNoResults_503()
route: new RouteInfo("route1"),
proxyRoute: new ProxyRoute(),
cluster: cluster1,
- aspNetCoreEndpoints: aspNetCoreEndpoints.AsReadOnly(),
transforms: null);
var aspNetCoreEndpoint = CreateAspNetCoreEndpoint(routeConfig);
aspNetCoreEndpoints.Add(aspNetCoreEndpoint);
diff --git a/test/ReverseProxy.Tests/Middleware/ProxyInvokerMiddlewareTests.cs b/test/ReverseProxy.Tests/Middleware/ProxyInvokerMiddlewareTests.cs
index d65c0036e1..e5e67c77b7 100644
--- a/test/ReverseProxy.Tests/Middleware/ProxyInvokerMiddlewareTests.cs
+++ b/test/ReverseProxy.Tests/Middleware/ProxyInvokerMiddlewareTests.cs
@@ -67,7 +67,6 @@ public async Task Invoke_Works()
route: new RouteInfo("route1"),
proxyRoute: new ProxyRoute(),
cluster: cluster1,
- aspNetCoreEndpoints: aspNetCoreEndpoints.AsReadOnly(),
transforms: null);
var aspNetCoreEndpoint = CreateAspNetCoreEndpoint(routeConfig);
aspNetCoreEndpoints.Add(aspNetCoreEndpoint);
@@ -137,7 +136,6 @@ public async Task NoDestinations_503()
route: new RouteInfo("route1"),
proxyRoute: new ProxyRoute(),
cluster: cluster1,
- aspNetCoreEndpoints: aspNetCoreEndpoints.AsReadOnly(),
transforms: null);
var aspNetCoreEndpoint = CreateAspNetCoreEndpoint(routeConfig);
aspNetCoreEndpoints.Add(aspNetCoreEndpoint);
diff --git a/test/ReverseProxy.Tests/Service/Config/RuntimeRouteBuilderTests.cs b/test/ReverseProxy.Tests/Service/Config/RuntimeRouteBuilderTests.cs
index e334efde23..15847a6188 100644
--- a/test/ReverseProxy.Tests/Service/Config/RuntimeRouteBuilderTests.cs
+++ b/test/ReverseProxy.Tests/Service/Config/RuntimeRouteBuilderTests.cs
@@ -37,11 +37,10 @@ public void Constructor_Works()
}
[Fact]
- public void BuildEndpoints_HostAndPath_Works()
+ public void BuildEndpoints_Works()
{
var services = CreateServices();
var builder = services.GetRequiredService();
- builder.SetProxyPipeline(context => Task.CompletedTask);
var route = new ProxyRoute
{
@@ -59,351 +58,11 @@ public void BuildEndpoints_HostAndPath_Works()
var config = builder.Build(route, cluster, routeInfo);
Assert.Same(cluster, config.Cluster);
+ Assert.Same(route, config.ProxyRoute);
+ Assert.Same(routeInfo, config.Route);
Assert.Equal(12, config.Order);
- Assert.Single(config.Endpoints);
- var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint;
- Assert.Equal("route1", routeEndpoint.DisplayName);
- Assert.Same(config, routeEndpoint.Metadata.GetMetadata());
- Assert.Equal("/a", routeEndpoint.RoutePattern.RawText);
- Assert.Equal(12, routeEndpoint.Order);
+
Assert.False(config.HasConfigChanged(route, cluster));
-
- var hostMetadata = routeEndpoint.Metadata.GetMetadata();
- Assert.NotNull(hostMetadata);
- Assert.Single(hostMetadata.Hosts);
- Assert.Equal("example.com", hostMetadata.Hosts[0]);
- }
-
- [Fact]
- public void BuildEndpoints_JustHost_Works()
- {
- var services = CreateServices();
- var builder = services.GetRequiredService();
- builder.SetProxyPipeline(context => Task.CompletedTask);
-
- var route = new ProxyRoute
- {
- RouteId = "route1",
- Match =
- {
- Hosts = new[] { "example.com" },
- },
- Order = 12,
- };
- var cluster = new ClusterInfo("cluster1", new DestinationManager());
- var routeInfo = new RouteInfo("route1");
-
- var config = builder.Build(route, cluster, routeInfo);
-
- Assert.Same(cluster, config.Cluster);
- Assert.Equal(12, config.Order);
- Assert.Single(config.Endpoints);
- var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint;
- Assert.Equal("route1", routeEndpoint.DisplayName);
- Assert.Same(config, routeEndpoint.Metadata.GetMetadata());
- Assert.Equal("/{**catchall}", routeEndpoint.RoutePattern.RawText);
- Assert.Equal(12, routeEndpoint.Order);
- Assert.False(config.HasConfigChanged(route, cluster));
-
- var hostMetadata = routeEndpoint.Metadata.GetMetadata();
- Assert.NotNull(hostMetadata);
- Assert.Single(hostMetadata.Hosts);
- Assert.Equal("example.com", hostMetadata.Hosts[0]);
- }
-
- [Fact]
- public void BuildEndpoints_JustHostWithWildcard_Works()
- {
- var services = CreateServices();
- var builder = services.GetRequiredService();
- builder.SetProxyPipeline(context => Task.CompletedTask);
-
- var route = new ProxyRoute
- {
- RouteId = "route1",
- Match =
- {
- Hosts = new[] { "*.example.com" },
- },
- Order = 12,
- };
- var cluster = new ClusterInfo("cluster1", new DestinationManager());
- var routeInfo = new RouteInfo("route1");
-
- var config = builder.Build(route, cluster, routeInfo);
-
- Assert.Same(cluster, config.Cluster);
- Assert.Equal(12, config.Order);
- Assert.Single(config.Endpoints);
- var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint;
- Assert.Equal("route1", routeEndpoint.DisplayName);
- Assert.Same(config, routeEndpoint.Metadata.GetMetadata());
- Assert.Equal("/{**catchall}", routeEndpoint.RoutePattern.RawText);
- Assert.Equal(12, routeEndpoint.Order);
- Assert.False(config.HasConfigChanged(route, cluster));
-
- var hostMetadata = routeEndpoint.Metadata.GetMetadata();
- Assert.NotNull(hostMetadata);
- Assert.Single(hostMetadata.Hosts);
- Assert.Equal("*.example.com", hostMetadata.Hosts[0]);
- }
-
- [Fact]
- public void BuildEndpoints_JustPath_Works()
- {
- var services = CreateServices();
- var builder = services.GetRequiredService();
- builder.SetProxyPipeline(context => Task.CompletedTask);
-
- var route = new ProxyRoute
- {
- RouteId = "route1",
- Match =
- {
- Path = "/a",
- },
- Order = 12,
- };
- var cluster = new ClusterInfo("cluster1", new DestinationManager());
- var routeInfo = new RouteInfo("route1");
-
- var config = builder.Build(route, cluster, routeInfo);
-
- Assert.Same(cluster, config.Cluster);
- Assert.Equal(12, config.Order);
- Assert.Single(config.Endpoints);
- var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint;
- Assert.Equal("route1", routeEndpoint.DisplayName);
- Assert.Same(config, routeEndpoint.Metadata.GetMetadata());
- Assert.Equal("/a", routeEndpoint.RoutePattern.RawText);
- Assert.Equal(12, routeEndpoint.Order);
- Assert.False(config.HasConfigChanged(route, cluster));
-
- var hostMetadata = routeEndpoint.Metadata.GetMetadata();
- Assert.Null(hostMetadata);
- }
-
- [Fact]
- public void BuildEndpoints_NullMatchers_Works()
- {
- var services = CreateServices();
- var builder = services.GetRequiredService();
- builder.SetProxyPipeline(context => Task.CompletedTask);
-
- var route = new ProxyRoute
- {
- RouteId = "route1",
- Order = 12,
- };
- var cluster = new ClusterInfo("cluster1", new DestinationManager());
- var routeInfo = new RouteInfo("route1");
-
- var config = builder.Build(route, cluster, routeInfo);
-
- Assert.Same(cluster, config.Cluster);
- Assert.Equal(12, config.Order);
- Assert.Single(config.Endpoints);
- var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint;
- Assert.Equal("route1", routeEndpoint.DisplayName);
- Assert.Same(config, routeEndpoint.Metadata.GetMetadata());
- Assert.Equal("/{**catchall}", routeEndpoint.RoutePattern.RawText);
- Assert.Equal(12, routeEndpoint.Order);
- Assert.False(config.HasConfigChanged(route, cluster));
-
- var hostMetadata = routeEndpoint.Metadata.GetMetadata();
- Assert.Null(hostMetadata);
- }
-
- [Fact]
- public void BuildEndpoints_InvalidPath_BubblesOutException()
- {
- var services = CreateServices();
- var builder = services.GetRequiredService();
- builder.SetProxyPipeline(context => Task.CompletedTask);
-
- var route = new ProxyRoute
- {
- RouteId = "route1",
- Match =
- {
- Path = "/{invalid",
- },
- Order = 12,
- };
- var cluster = new ClusterInfo("cluster1", new DestinationManager());
- var routeInfo = new RouteInfo("route1");
-
- Action action = () => builder.Build(route, cluster, routeInfo);
-
- Assert.Throws(action);
- }
-
- [Fact]
- public void BuildEndpoints_DefaultAuth_Works()
- {
- var services = CreateServices();
- var builder = services.GetRequiredService();
- builder.SetProxyPipeline(context => Task.CompletedTask);
-
- var route = new ProxyRoute
- {
- RouteId = "route1",
- AuthorizationPolicy = "defaulT",
- Order = 12,
- };
- var cluster = new ClusterInfo("cluster1", new DestinationManager());
- var routeInfo = new RouteInfo("route1");
-
- var config = builder.Build(route, cluster, routeInfo);
-
- Assert.Single(config.Endpoints);
- var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint;
- var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata());
- Assert.Null(attribute.Policy);
- }
-
- [Fact]
- public void BuildEndpoints_CustomAuth_Works()
- {
- var services = CreateServices();
- var builder = services.GetRequiredService();
- builder.SetProxyPipeline(context => Task.CompletedTask);
-
- var route = new ProxyRoute
- {
- RouteId = "route1",
- AuthorizationPolicy = "custom",
- Order = 12,
- };
- var cluster = new ClusterInfo("cluster1", new DestinationManager());
- var routeInfo = new RouteInfo("route1");
-
- var config = builder.Build(route, cluster, routeInfo);
-
- Assert.Single(config.Endpoints);
- var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint;
- var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata());
- Assert.Equal("custom", attribute.Policy);
- }
-
- [Fact]
- public void BuildEndpoints_NoAuth_Works()
- {
- var services = CreateServices();
- var builder = services.GetRequiredService();
- builder.SetProxyPipeline(context => Task.CompletedTask);
-
- var route = new ProxyRoute
- {
- RouteId = "route1",
- Order = 12,
- };
- var cluster = new ClusterInfo("cluster1", new DestinationManager());
- var routeInfo = new RouteInfo("route1");
-
- var config = builder.Build(route, cluster, routeInfo);
-
- Assert.Single(config.Endpoints);
- var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint;
- Assert.Null(routeEndpoint.Metadata.GetMetadata());
- Assert.Null(routeEndpoint.Metadata.GetMetadata());
- }
-
- [Fact]
- public void BuildEndpoints_DefaultCors_Works()
- {
- var services = CreateServices();
- var builder = services.GetRequiredService();
- builder.SetProxyPipeline(context => Task.CompletedTask);
-
- var route = new ProxyRoute
- {
- RouteId = "route1",
- CorsPolicy = "defaulT",
- Order = 12,
- };
- var cluster = new ClusterInfo("cluster1", new DestinationManager());
- var routeInfo = new RouteInfo("route1");
-
- var config = builder.Build(route, cluster, routeInfo);
-
- Assert.Single(config.Endpoints);
- var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint;
- var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata());
- Assert.Null(attribute.PolicyName);
- Assert.Null(routeEndpoint.Metadata.GetMetadata());
- }
-
- [Fact]
- public void BuildEndpoints_CustomCors_Works()
- {
- var services = CreateServices();
- var builder = services.GetRequiredService();
- builder.SetProxyPipeline(context => Task.CompletedTask);
-
- var route = new ProxyRoute
- {
- RouteId = "route1",
- CorsPolicy = "custom",
- Order = 12,
- };
- var cluster = new ClusterInfo("cluster1", new DestinationManager());
- var routeInfo = new RouteInfo("route1");
-
- var config = builder.Build(route, cluster, routeInfo);
-
- Assert.Single(config.Endpoints);
- var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint;
- var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata());
- Assert.Equal("custom", attribute.PolicyName);
- Assert.Null(routeEndpoint.Metadata.GetMetadata());
- }
-
- [Fact]
- public void BuildEndpoints_DisableCors_Works()
- {
- var services = CreateServices();
- var builder = services.GetRequiredService();
- builder.SetProxyPipeline(context => Task.CompletedTask);
-
- var route = new ProxyRoute
- {
- RouteId = "route1",
- CorsPolicy = "disAble",
- Order = 12,
- };
- var cluster = new ClusterInfo("cluster1", new DestinationManager());
- var routeInfo = new RouteInfo("route1");
-
- var config = builder.Build(route, cluster, routeInfo);
-
- Assert.Single(config.Endpoints);
- var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint;
- Assert.IsType(routeEndpoint.Metadata.GetMetadata());
- Assert.Null(routeEndpoint.Metadata.GetMetadata());
- }
-
- [Fact]
- public void BuildEndpoints_NoCors_Works()
- {
- var services = CreateServices();
- var builder = services.GetRequiredService();
- builder.SetProxyPipeline(context => Task.CompletedTask);
-
- var route = new ProxyRoute
- {
- RouteId = "route1",
- Order = 12,
- };
- var cluster = new ClusterInfo("cluster1", new DestinationManager());
- var routeInfo = new RouteInfo("route1");
-
- var config = builder.Build(route, cluster, routeInfo);
-
- Assert.Single(config.Endpoints);
- var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint;
- Assert.Null(routeEndpoint.Metadata.GetMetadata());
- Assert.Null(routeEndpoint.Metadata.GetMetadata());
}
}
}
diff --git a/test/ReverseProxy.Tests/Service/DynamicEndpoint/ProxyEndpointFactorTests.cs b/test/ReverseProxy.Tests/Service/DynamicEndpoint/ProxyEndpointFactorTests.cs
new file mode 100644
index 0000000000..5e5911b4ef
--- /dev/null
+++ b/test/ReverseProxy.Tests/Service/DynamicEndpoint/ProxyEndpointFactorTests.cs
@@ -0,0 +1,396 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Cors;
+using Microsoft.AspNetCore.Cors.Infrastructure;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.ReverseProxy.Abstractions;
+using Microsoft.ReverseProxy.RuntimeModel;
+using Microsoft.ReverseProxy.Service.Management;
+using Microsoft.ReverseProxy.Service.RuntimeModel.Transforms;
+using Xunit;
+
+namespace Microsoft.ReverseProxy.Service.DynamicEndpoint
+{
+ public class ProxyEndpointFactorTests
+ {
+ private IServiceProvider CreateServices()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddSingleton();
+ return serviceCollection.BuildServiceProvider();
+ }
+
+ [Fact]
+ public void Constructor_Works()
+ {
+ var services = CreateServices();
+ _ = services.GetRequiredService();
+ }
+
+ [Fact]
+ public void AddEndpoint_HostAndPath_Works()
+ {
+ var services = CreateServices();
+ var factory = services.GetRequiredService();
+ factory.SetProxyPipeline(context => Task.CompletedTask);
+
+ var route = new ProxyRoute
+ {
+ RouteId = "route1",
+ Match =
+ {
+ Hosts = new[] { "example.com" },
+ Path = "/a",
+ },
+ Order = 12,
+ };
+ var cluster = new ClusterInfo("cluster1", new DestinationManager());
+ var routeInfo = new RouteInfo("route1");
+
+ var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeInfo, route, cluster);
+
+ Assert.Same(cluster, routeConfig.Cluster);
+ Assert.Equal(12, routeConfig.Order);
+ Assert.Equal("route1", routeEndpoint.DisplayName);
+ Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata());
+ Assert.Equal("/a", routeEndpoint.RoutePattern.RawText);
+ Assert.Equal(12, routeEndpoint.Order);
+ Assert.False(routeConfig.HasConfigChanged(route, cluster));
+
+ var hostMetadata = routeEndpoint.Metadata.GetMetadata();
+ Assert.NotNull(hostMetadata);
+ Assert.Single(hostMetadata.Hosts);
+ Assert.Equal("example.com", hostMetadata.Hosts[0]);
+ }
+
+ private (RouteEndpoint routeEndpoint, RouteConfig routeConfig) CreateEndpoint(ProxyEndpointFactory factory, RouteInfo routeInfo, ProxyRoute proxyRoute, ClusterInfo clusterInfo)
+ {
+ var routeConfig = new RouteConfig(routeInfo, proxyRoute, clusterInfo, Transforms.Empty);
+ var endpoints = new List();
+
+ factory.AddEndpoint(endpoints, routeConfig, Array.Empty>());
+
+ var endpoint = Assert.Single(endpoints);
+ var routeEndpoint = Assert.IsType(endpoint);
+
+ return (routeEndpoint, routeConfig);
+ }
+
+ [Fact]
+ public void AddEndpoint_JustHost_Works()
+ {
+ var services = CreateServices();
+ var factory = services.GetRequiredService();
+ factory.SetProxyPipeline(context => Task.CompletedTask);
+
+ var route = new ProxyRoute
+ {
+ RouteId = "route1",
+ Match =
+ {
+ Hosts = new[] { "example.com" },
+ },
+ Order = 12,
+ };
+ var cluster = new ClusterInfo("cluster1", new DestinationManager());
+ var routeInfo = new RouteInfo("route1");
+
+ var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeInfo, route, cluster);
+
+ Assert.Same(cluster, routeConfig.Cluster);
+ Assert.Equal(12, routeConfig.Order);
+ Assert.Equal("route1", routeEndpoint.DisplayName);
+ Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata());
+ Assert.Equal("/{**catchall}", routeEndpoint.RoutePattern.RawText);
+ Assert.Equal(12, routeEndpoint.Order);
+ Assert.False(routeConfig.HasConfigChanged(route, cluster));
+
+ var hostMetadata = routeEndpoint.Metadata.GetMetadata();
+ Assert.NotNull(hostMetadata);
+ Assert.Single(hostMetadata.Hosts);
+ Assert.Equal("example.com", hostMetadata.Hosts[0]);
+ }
+
+ [Fact]
+ public void AddEndpoint_JustHostWithWildcard_Works()
+ {
+ var services = CreateServices();
+ var factory = services.GetRequiredService();
+ factory.SetProxyPipeline(context => Task.CompletedTask);
+
+ var route = new ProxyRoute
+ {
+ RouteId = "route1",
+ Match =
+ {
+ Hosts = new[] { "*.example.com" },
+ },
+ Order = 12,
+ };
+ var cluster = new ClusterInfo("cluster1", new DestinationManager());
+ var routeInfo = new RouteInfo("route1");
+
+ var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeInfo, route, cluster);
+
+ Assert.Same(cluster, routeConfig.Cluster);
+ Assert.Equal(12, routeConfig.Order);
+ Assert.Equal("route1", routeEndpoint.DisplayName);
+ Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata());
+ Assert.Equal("/{**catchall}", routeEndpoint.RoutePattern.RawText);
+ Assert.Equal(12, routeEndpoint.Order);
+ Assert.False(routeConfig.HasConfigChanged(route, cluster));
+
+ var hostMetadata = routeEndpoint.Metadata.GetMetadata();
+ Assert.NotNull(hostMetadata);
+ Assert.Single(hostMetadata.Hosts);
+ Assert.Equal("*.example.com", hostMetadata.Hosts[0]);
+ }
+
+ [Fact]
+ public void AddEndpoint_JustPath_Works()
+ {
+ var services = CreateServices();
+ var factory = services.GetRequiredService();
+ factory.SetProxyPipeline(context => Task.CompletedTask);
+
+ var route = new ProxyRoute
+ {
+ RouteId = "route1",
+ Match =
+ {
+ Path = "/a",
+ },
+ Order = 12,
+ };
+ var cluster = new ClusterInfo("cluster1", new DestinationManager());
+ var routeInfo = new RouteInfo("route1");
+
+ var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeInfo, route, cluster);
+
+ Assert.Same(cluster, routeConfig.Cluster);
+ Assert.Equal(12, routeConfig.Order);
+ Assert.Equal("route1", routeEndpoint.DisplayName);
+ Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata());
+ Assert.Equal("/a", routeEndpoint.RoutePattern.RawText);
+ Assert.Equal(12, routeEndpoint.Order);
+ Assert.False(routeConfig.HasConfigChanged(route, cluster));
+
+ var hostMetadata = routeEndpoint.Metadata.GetMetadata();
+ Assert.Null(hostMetadata);
+ }
+
+ [Fact]
+ public void AddEndpoint_NullMatchers_Works()
+ {
+ var services = CreateServices();
+ var factory = services.GetRequiredService();
+ factory.SetProxyPipeline(context => Task.CompletedTask);
+
+ var route = new ProxyRoute
+ {
+ RouteId = "route1",
+ Order = 12,
+ };
+ var cluster = new ClusterInfo("cluster1", new DestinationManager());
+ var routeInfo = new RouteInfo("route1");
+
+ var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeInfo, route, cluster);
+
+ Assert.Same(cluster, routeConfig.Cluster);
+ Assert.Equal(12, routeConfig.Order);
+ Assert.Equal("route1", routeEndpoint.DisplayName);
+ Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata());
+ Assert.Equal("/{**catchall}", routeEndpoint.RoutePattern.RawText);
+ Assert.Equal(12, routeEndpoint.Order);
+ Assert.False(routeConfig.HasConfigChanged(route, cluster));
+
+ var hostMetadata = routeEndpoint.Metadata.GetMetadata();
+ Assert.Null(hostMetadata);
+ }
+
+ [Fact]
+ public void AddEndpoint_InvalidPath_BubblesOutException()
+ {
+ var services = CreateServices();
+ var factory = services.GetRequiredService();
+ factory.SetProxyPipeline(context => Task.CompletedTask);
+
+ var route = new ProxyRoute
+ {
+ RouteId = "route1",
+ Match =
+ {
+ Path = "/{invalid",
+ },
+ Order = 12,
+ };
+ var cluster = new ClusterInfo("cluster1", new DestinationManager());
+ var routeInfo = new RouteInfo("route1");
+
+ Action action = () => CreateEndpoint(factory, routeInfo, route, cluster);
+
+ Assert.Throws(action);
+ }
+
+ [Fact]
+ public void AddEndpoint_DefaultAuth_Works()
+ {
+ var services = CreateServices();
+ var factory = services.GetRequiredService();
+ factory.SetProxyPipeline(context => Task.CompletedTask);
+
+ var route = new ProxyRoute
+ {
+ RouteId = "route1",
+ AuthorizationPolicy = "defaulT",
+ Order = 12,
+ };
+ var cluster = new ClusterInfo("cluster1", new DestinationManager());
+ var routeInfo = new RouteInfo("route1");
+
+ var (routeEndpoint, _) = CreateEndpoint(factory, routeInfo, route, cluster);
+
+ var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata());
+ Assert.Null(attribute.Policy);
+ }
+
+ [Fact]
+ public void AddEndpoint_CustomAuth_Works()
+ {
+ var services = CreateServices();
+ var factory = services.GetRequiredService();
+ factory.SetProxyPipeline(context => Task.CompletedTask);
+
+ var route = new ProxyRoute
+ {
+ RouteId = "route1",
+ AuthorizationPolicy = "custom",
+ Order = 12,
+ };
+ var cluster = new ClusterInfo("cluster1", new DestinationManager());
+ var routeInfo = new RouteInfo("route1");
+
+ var (routeEndpoint, _) = CreateEndpoint(factory, routeInfo, route, cluster);
+
+ var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata());
+ Assert.Equal("custom", attribute.Policy);
+ }
+
+ [Fact]
+ public void AddEndpoint_NoAuth_Works()
+ {
+ var services = CreateServices();
+ var factory = services.GetRequiredService();
+ factory.SetProxyPipeline(context => Task.CompletedTask);
+
+ var route = new ProxyRoute
+ {
+ RouteId = "route1",
+ Order = 12,
+ };
+ var cluster = new ClusterInfo("cluster1", new DestinationManager());
+ var routeInfo = new RouteInfo("route1");
+
+ var (routeEndpoint, _) = CreateEndpoint(factory, routeInfo, route, cluster);
+
+ Assert.Null(routeEndpoint.Metadata.GetMetadata());
+ Assert.Null(routeEndpoint.Metadata.GetMetadata());
+ }
+
+ [Fact]
+ public void AddEndpoint_DefaultCors_Works()
+ {
+ var services = CreateServices();
+ var factory = services.GetRequiredService();
+ factory.SetProxyPipeline(context => Task.CompletedTask);
+
+ var route = new ProxyRoute
+ {
+ RouteId = "route1",
+ CorsPolicy = "defaulT",
+ Order = 12,
+ };
+ var cluster = new ClusterInfo("cluster1", new DestinationManager());
+ var routeInfo = new RouteInfo("route1");
+
+ var (routeEndpoint, _) = CreateEndpoint(factory, routeInfo, route, cluster);
+
+ var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata());
+ Assert.Null(attribute.PolicyName);
+ Assert.Null(routeEndpoint.Metadata.GetMetadata());
+ }
+
+ [Fact]
+ public void AddEndpoint_CustomCors_Works()
+ {
+ var services = CreateServices();
+ var factory = services.GetRequiredService();
+ factory.SetProxyPipeline(context => Task.CompletedTask);
+
+ var route = new ProxyRoute
+ {
+ RouteId = "route1",
+ CorsPolicy = "custom",
+ Order = 12,
+ };
+ var cluster = new ClusterInfo("cluster1", new DestinationManager());
+ var routeInfo = new RouteInfo("route1");
+
+ var (routeEndpoint, _) = CreateEndpoint(factory, routeInfo, route, cluster);
+
+ var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata());
+ Assert.Equal("custom", attribute.PolicyName);
+ Assert.Null(routeEndpoint.Metadata.GetMetadata());
+ }
+
+ [Fact]
+ public void AddEndpoint_DisableCors_Works()
+ {
+ var services = CreateServices();
+ var factory = services.GetRequiredService();
+ factory.SetProxyPipeline(context => Task.CompletedTask);
+
+ var route = new ProxyRoute
+ {
+ RouteId = "route1",
+ CorsPolicy = "disAble",
+ Order = 12,
+ };
+ var cluster = new ClusterInfo("cluster1", new DestinationManager());
+ var routeInfo = new RouteInfo("route1");
+
+ var (routeEndpoint, _) = CreateEndpoint(factory, routeInfo, route, cluster);
+
+ Assert.IsType(routeEndpoint.Metadata.GetMetadata());
+ Assert.Null(routeEndpoint.Metadata.GetMetadata());
+ }
+
+ [Fact]
+ public void AddEndpoint_NoCors_Works()
+ {
+ var services = CreateServices();
+ var factory = services.GetRequiredService();
+ factory.SetProxyPipeline(context => Task.CompletedTask);
+
+ var route = new ProxyRoute
+ {
+ RouteId = "route1",
+ Order = 12,
+ };
+ var cluster = new ClusterInfo("cluster1", new DestinationManager());
+ var routeInfo = new RouteInfo("route1");
+
+ var (routeEndpoint, _) = CreateEndpoint(factory, routeInfo, route, cluster);
+
+ Assert.Null(routeEndpoint.Metadata.GetMetadata());
+ Assert.Null(routeEndpoint.Metadata.GetMetadata());
+ }
+ }
+}
diff --git a/test/ReverseProxy.Tests/Service/Management/ProxyConfigManagerTests.cs b/test/ReverseProxy.Tests/Service/Management/ProxyConfigManagerTests.cs
index 3c4aee2a73..907637a947 100644
--- a/test/ReverseProxy.Tests/Service/Management/ProxyConfigManagerTests.cs
+++ b/test/ReverseProxy.Tests/Service/Management/ProxyConfigManagerTests.cs
@@ -31,7 +31,7 @@ private IServiceProvider CreateServices(List routes, List c
serviceCollection.TryAddSingleton(new Mock().Object);
configureProxy?.Invoke(proxyBuilder);
var services = serviceCollection.BuildServiceProvider();
- var routeBuilder = services.GetRequiredService();
+ var routeBuilder = services.GetRequiredService();
routeBuilder.SetProxyPipeline(context => Task.CompletedTask);
return services;
}