();
+
+// Configure the reverse proxy.
+builder.Services.ConfigureReverseProxy(builder.Configuration);
+
+var app = builder.Build();
+
+// Use the configured reverse proxy.
+app.UseReverseProxy();
+
+// No further routes are registered. This means that you will get a 404 error status on all requests
+// that do not match a proxied Aspire resource.
+await app.RunAsync();
diff --git a/examples/Aspire.ReverseProxy/Properties/launchSettings.json b/examples/Aspire.ReverseProxy/Properties/launchSettings.json
new file mode 100644
index 0000000..b2838f7
--- /dev/null
+++ b/examples/Aspire.ReverseProxy/Properties/launchSettings.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "applicationUrl": "https://localhost:443"
+ }
+ }
+}
diff --git a/examples/Aspire.ReverseProxy/appsettings.json b/examples/Aspire.ReverseProxy/appsettings.json
new file mode 100644
index 0000000..5de8cf3
--- /dev/null
+++ b/examples/Aspire.ReverseProxy/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "SelfSignedCertificate": {
+ "CaFilePath": "c:/"
+ }
+}
diff --git a/examples/Aspire.ServiceDefaults/Examples.Aspire.ServiceDefaults.csproj b/examples/Aspire.ServiceDefaults/Examples.Aspire.ServiceDefaults.csproj
new file mode 100644
index 0000000..ddd82c9
--- /dev/null
+++ b/examples/Aspire.ServiceDefaults/Examples.Aspire.ServiceDefaults.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net9.0
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/Aspire.ServiceDefaults/Extensions.cs b/examples/Aspire.ServiceDefaults/Extensions.cs
new file mode 100644
index 0000000..bcb8c68
--- /dev/null
+++ b/examples/Aspire.ServiceDefaults/Extensions.cs
@@ -0,0 +1,94 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Diagnostics.HealthChecks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+
+namespace Hj.Examples.Aspire.ServiceDefaults;
+
+public static class Extensions
+{
+ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
+ {
+ builder.ConfigureOpenTelemetry();
+
+ builder.AddDefaultHealthChecks();
+
+ builder.Services.AddServiceDiscovery();
+
+ builder.Services.ConfigureHttpClientDefaults(http =>
+ {
+ http.AddStandardResilienceHandler();
+
+ http.AddServiceDiscovery();
+ });
+
+ return builder;
+ }
+
+ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
+ {
+ builder.Logging.AddOpenTelemetry(logging =>
+ {
+ logging.IncludeFormattedMessage = true;
+ logging.IncludeScopes = true;
+ });
+
+ builder.Services.AddOpenTelemetry()
+ .WithMetrics(metrics =>
+ {
+ metrics
+ .AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation();
+ })
+ .WithTracing(tracing =>
+ {
+ tracing
+ .AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation();
+ });
+
+ builder.AddOpenTelemetryExporters();
+
+ return builder;
+ }
+
+ public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)
+ {
+ builder.Services
+ .AddHealthChecks()
+ .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
+
+ return builder;
+ }
+
+ public static WebApplication MapDefaultEndpoints(this WebApplication app)
+ {
+ app.MapHealthChecks("/health");
+ app.MapHealthChecks("/alive", new HealthCheckOptions
+ {
+ Predicate = r => r.Tags.Contains("live"),
+ });
+
+ return app;
+ }
+
+ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
+ {
+ var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
+
+ if (useOtlpExporter)
+ {
+ builder.Services
+ .AddOpenTelemetry()
+ .UseOtlpExporter();
+ }
+
+ return builder;
+ }
+}
diff --git a/examples/Aspire.Website/Controllers/HomeController.cs b/examples/Aspire.Website/Controllers/HomeController.cs
new file mode 100644
index 0000000..516df09
--- /dev/null
+++ b/examples/Aspire.Website/Controllers/HomeController.cs
@@ -0,0 +1,8 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Hj.Examples.Aspire.Website.Controllers;
+
+public class HomeController : Controller
+{
+ public IActionResult Index() => View();
+}
diff --git a/examples/Aspire.Website/Examples.Aspire.Website.csproj b/examples/Aspire.Website/Examples.Aspire.Website.csproj
new file mode 100644
index 0000000..cb2d44b
--- /dev/null
+++ b/examples/Aspire.Website/Examples.Aspire.Website.csproj
@@ -0,0 +1,12 @@
+
+
+
+ net9.0
+ true
+
+
+
+
+
+
+
diff --git a/examples/Aspire.Website/Program.cs b/examples/Aspire.Website/Program.cs
new file mode 100644
index 0000000..bf0f828
--- /dev/null
+++ b/examples/Aspire.Website/Program.cs
@@ -0,0 +1,24 @@
+using Hj.Examples.Aspire.ServiceDefaults;
+using Microsoft.AspNetCore.HttpOverrides;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+builder.Services.AddControllersWithViews();
+
+var app = builder.Build();
+
+// Enable handling forwarded headers.
+app.UseForwardedHeaders(new ForwardedHeadersOptions
+{
+ ForwardedHeaders = ForwardedHeaders.All,
+});
+
+app.MapDefaultEndpoints();
+
+app.UseRouting();
+
+app.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}");
+
+await app.RunAsync();
diff --git a/examples/Aspire.Website/Properties/launchSettings.json b/examples/Aspire.Website/Properties/launchSettings.json
new file mode 100644
index 0000000..b4ace90
--- /dev/null
+++ b/examples/Aspire.Website/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "profiles": {
+ "Examples.Aspire.Website": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:60013;http://localhost:60014"
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/Aspire.Website/Views/Home/Index.cshtml b/examples/Aspire.Website/Views/Home/Index.cshtml
new file mode 100644
index 0000000..f31a46c
--- /dev/null
+++ b/examples/Aspire.Website/Views/Home/Index.cshtml
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Website - Example
+
+
+ Website
+ You should see this page when navigating to "https://example-website.local/".
+ This website do not use SSL. Instead, a secure HTTPS connection is provided by the reverse proxy.
+ In case "example-website.local" cannot be found then you need to map it to 127.0.0.1 and ::1 in your local hosts file.
+
+
diff --git a/examples/Aspire.Website/appsettings.Development.json b/examples/Aspire.Website/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/examples/Aspire.Website/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/examples/Aspire.Website/appsettings.json b/examples/Aspire.Website/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/examples/Aspire.Website/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/examples/Aspire.Website/wwwroot/favicon.ico b/examples/Aspire.Website/wwwroot/favicon.ico
new file mode 100644
index 0000000..63e859b
Binary files /dev/null and b/examples/Aspire.Website/wwwroot/favicon.ico differ
diff --git a/src/ReverseProxy.Aspire/Constants.cs b/src/ReverseProxy.Aspire/Constants.cs
new file mode 100644
index 0000000..78fa0f4
--- /dev/null
+++ b/src/ReverseProxy.Aspire/Constants.cs
@@ -0,0 +1,22 @@
+//
+// Copyright 2025 Henrik Jensen
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+namespace Hj.ReverseProxy.Aspire;
+
+internal static class Constants
+{
+ public const string EnvPrefix = "reverseproxy";
+}
diff --git a/src/ReverseProxy.Aspire/README.md b/src/ReverseProxy.Aspire/README.md
new file mode 100644
index 0000000..ea6e116
--- /dev/null
+++ b/src/ReverseProxy.Aspire/README.md
@@ -0,0 +1,6 @@
+# A Reverse Proxy for .NET Aspire.
+This package integrates the HenrikJensen.ReverseProxy package into a .NET Aspire solution.
+
+It contains an "WithReverseProxyReference" annotation for configuring Aspire resources in the Program.cs of the AppHost.
+
+It also contains a start up filter to be used in the website that acts as the reverse proxy. Registering the "ServiceDiscoveryStartupFilter" will configure the reverse proxy website to expose annotated resources.
diff --git a/src/ReverseProxy.Aspire/ResourceBuilderExtensions.cs b/src/ReverseProxy.Aspire/ResourceBuilderExtensions.cs
new file mode 100644
index 0000000..7d68f92
--- /dev/null
+++ b/src/ReverseProxy.Aspire/ResourceBuilderExtensions.cs
@@ -0,0 +1,36 @@
+//
+// Copyright 2025 Henrik Jensen
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+namespace Hj.ReverseProxy.Aspire;
+
+public static class ResourceBuilderExtensions
+{
+ public static IResourceBuilder WithReverseProxyReference(this IResourceBuilder builder, string serviceName, EndpointReference endpointReference, string hostName, int port = 443)
+ where T : IResourceWithEnvironment
+ {
+ var externalUrl = $"https://{hostName}:{port}";
+
+ builder
+ .WithUrl(externalUrl)
+ .WithReference(endpointReference)
+ .WithEnvironment(context =>
+ {
+ context.EnvironmentVariables[$"{Constants.EnvPrefix}__{serviceName}"] = hostName;
+ });
+
+ return builder;
+ }
+}
diff --git a/src/ReverseProxy.Aspire/ReverseProxy.Aspire.csproj b/src/ReverseProxy.Aspire/ReverseProxy.Aspire.csproj
new file mode 100644
index 0000000..99c7e49
--- /dev/null
+++ b/src/ReverseProxy.Aspire/ReverseProxy.Aspire.csproj
@@ -0,0 +1,38 @@
+
+
+
+ net8.0;net9.0
+ Recommended
+ true
+ true
+ true
+ snupkg
+ true
+ HenrikJensen.ReverseProxy.Aspire
+ .NET Aspire integration package.
+ testing;reverse-proxy;aspire
+ README.md
+ LICENSE
+ 1.0.0-beta1
+
+
+
+
+
+
+
+
+
+ <_Parameter1>$(MSBuildProjectName).UnitTest
+
+
+ <_Parameter1>DynamicProxyGenAssembly2
+
+
+
+
+
+
+
+
+
diff --git a/src/ReverseProxy.Aspire/ServiceDiscovery.cs b/src/ReverseProxy.Aspire/ServiceDiscovery.cs
new file mode 100644
index 0000000..1d8d4ef
--- /dev/null
+++ b/src/ReverseProxy.Aspire/ServiceDiscovery.cs
@@ -0,0 +1,80 @@
+//
+// Copyright 2025 Henrik Jensen
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using Microsoft.Extensions.Configuration;
+
+namespace Hj.ReverseProxy.Aspire;
+
+internal static class ServiceDiscovery
+{
+ public static IEnumerable<(string ServiceName, string HostName)> ReadConfiguration(IConfiguration configuration)
+ {
+ var reverseProxySection = configuration.GetSection(Constants.EnvPrefix);
+ foreach (var hostMapping in reverseProxySection.GetChildren())
+ {
+ var serviceName = hostMapping.Key;
+ var hostName = hostMapping.Value;
+ if (serviceName == null || hostName == null)
+ {
+ continue;
+ }
+
+ yield return (serviceName, hostName);
+ }
+ }
+
+ public static string[] DiscoverEndpointList(IConfiguration configuration, string query, string[]? allowedSchemes = null, bool allowAllSchemes = true)
+ {
+ if (!Uri.TryCreate(query, UriKind.Absolute, out var uri))
+ {
+ throw new InvalidOperationException($"Invalid service discovery query '{query}'");
+ }
+
+ var serviceName = uri.Host;
+ var namedEndpoint = string.Empty;
+ var namedEndpointSeparator = serviceName.IndexOf('.', StringComparison.Ordinal);
+ if (serviceName[0] == '_' && namedEndpointSeparator > 1 && serviceName[^1] != '.')
+ {
+ namedEndpoint = serviceName[1..namedEndpointSeparator];
+ serviceName = serviceName[(namedEndpointSeparator + 1)..];
+ }
+
+ allowedSchemes ??= uri.Scheme.Split('+');
+
+ ReadOnlySpan endpoints = [namedEndpoint, .. allowedSchemes];
+ foreach (var endpoint in endpoints)
+ {
+ var section = configuration.GetSection($"Services:{serviceName}:{endpoint}");
+ if (section.Exists())
+ {
+ var uriStrings = section.Get();
+ if (uriStrings == null || allowAllSchemes)
+ {
+ return uriStrings ?? [];
+ }
+
+ uriStrings = [.. uriStrings.Where(x =>
+ {
+ return Uri.TryCreate(x, default, out var uri)
+ && allowedSchemes.Contains(uri.Scheme, StringComparer.OrdinalIgnoreCase);
+ })];
+ return uriStrings;
+ }
+ }
+
+ return [];
+ }
+}
diff --git a/src/ReverseProxy.Aspire/ServiceDiscoveryStartupFilter.cs b/src/ReverseProxy.Aspire/ServiceDiscoveryStartupFilter.cs
new file mode 100644
index 0000000..08d99af
--- /dev/null
+++ b/src/ReverseProxy.Aspire/ServiceDiscoveryStartupFilter.cs
@@ -0,0 +1,84 @@
+//
+// Copyright 2025 Henrik Jensen
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Yarp.ReverseProxy.Configuration;
+
+namespace Hj.ReverseProxy.Aspire;
+
+public sealed class ServiceDiscoveryStartupFilter : IStartupFilter
+{
+ public Action Configure(Action next)
+ {
+ return app =>
+ {
+ using var scope = app.ApplicationServices.CreateScope();
+ var serviceProvider = scope.ServiceProvider;
+ ConfigureYarp(
+ serviceProvider.GetRequiredService(),
+ serviceProvider.GetRequiredService());
+
+ next(app);
+ };
+ }
+
+ private static void ConfigureYarp(
+ IConfiguration configuration,
+ InMemoryConfigProvider inMemoryConfigProvider)
+ {
+ List routes = [];
+ List clusters = [];
+
+ var hostMappings = ServiceDiscovery.ReadConfiguration(configuration);
+
+ foreach ((var serviceName, var hostName) in hostMappings)
+ {
+ var endpoints = ServiceDiscovery.DiscoverEndpointList(configuration, "https+http://" + serviceName);
+ if (endpoints.Length == 0)
+ {
+ continue;
+ }
+
+ var destinations = new Dictionary();
+ for (var i = 0; i < endpoints.Length; i++)
+ {
+ destinations.Add("destination" + i, new DestinationConfig() { Address = endpoints[i], });
+ }
+
+ clusters.Add(new ClusterConfig()
+ {
+ ClusterId = serviceName,
+ Destinations = destinations,
+ });
+
+ routes.Add(new RouteConfig()
+ {
+ RouteId = serviceName,
+ ClusterId = serviceName,
+ Match = new()
+ {
+ Hosts = [hostName],
+ Path = "/{**catch-all}",
+ },
+ });
+ }
+
+ inMemoryConfigProvider.Update(routes, clusters);
+ }
+}
diff --git a/src/ReverseProxy/Certificate/CertificateConstants.cs b/src/ReverseProxy/Certificate/CertificateConstants.cs
index 26b32bb..263df03 100644
--- a/src/ReverseProxy/Certificate/CertificateConstants.cs
+++ b/src/ReverseProxy/Certificate/CertificateConstants.cs
@@ -24,9 +24,9 @@ internal static class CertificateConstants
public const string DefaultCaSubjectName = "CN=ReverseProxy Root CA";
- public const string CaCrtFileName = "ca.crt.pem";
+ public const string CaCrtFileName = "ReverseProxy-RootCA.crt.pem";
- public const string CaKeyFileName = "ca.key.pem";
+ public const string CaKeyFileName = "ReverseProxy-RootCA.key.pem";
- public const string CaPfxFileName = "ca.pfx";
+ public const string CaPfxFileName = "ReverseProxy-RootCA.pfx";
}
diff --git a/src/ReverseProxy/Certificate/CertificateFactory.cs b/src/ReverseProxy/Certificate/CertificateFactory.cs
index c9235bd..fd94363 100644
--- a/src/ReverseProxy/Certificate/CertificateFactory.cs
+++ b/src/ReverseProxy/Certificate/CertificateFactory.cs
@@ -75,7 +75,9 @@ public X509Certificate2 CreateCertificate(AsymmetricAlgorithm key, X509Certifica
var pfxBytes = certificateWithKey.Export(X509ContentType.Pkcs12);
#pragma warning disable SYSLIB0057 // Type or member is obsolete
- var pfx = new X509Certificate2(pfxBytes, (string?)null, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.EphemeralKeySet);
+ var pfx = Environment.OSVersion.Platform == PlatformID.Win32NT
+ ? new X509Certificate2(pfxBytes, (string?)null, X509KeyStorageFlags.Exportable)
+ : new X509Certificate2(pfxBytes, (string?)null, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.EphemeralKeySet);
#pragma warning restore SYSLIB0057 // Type or member is obsolete
return pfx;
}
diff --git a/src/ReverseProxy/ReverseProxy.csproj b/src/ReverseProxy/ReverseProxy.csproj
index ccd31b5..54bce65 100644
--- a/src/ReverseProxy/ReverseProxy.csproj
+++ b/src/ReverseProxy/ReverseProxy.csproj
@@ -9,8 +9,11 @@
snupkg
true
HenrikJensen.ReverseProxy
+ Pairs Microsoft YARP with a web API for runtime configuration and automatic self-signed certificates.
+ testing;reverse-proxy;certificates
README.md
LICENSE
+ 1.0.0-beta3
diff --git a/test/ReverseProxy.Aspire.UnitTest/ReverseProxy.Aspire.UnitTest.csproj b/test/ReverseProxy.Aspire.UnitTest/ReverseProxy.Aspire.UnitTest.csproj
new file mode 100644
index 0000000..827f4f1
--- /dev/null
+++ b/test/ReverseProxy.Aspire.UnitTest/ReverseProxy.Aspire.UnitTest.csproj
@@ -0,0 +1,35 @@
+
+
+
+ net8.0;net9.0
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/test/ReverseProxy.Aspire.UnitTest/ServiceDiscoveryTests.cs b/test/ReverseProxy.Aspire.UnitTest/ServiceDiscoveryTests.cs
new file mode 100644
index 0000000..b287dc8
--- /dev/null
+++ b/test/ReverseProxy.Aspire.UnitTest/ServiceDiscoveryTests.cs
@@ -0,0 +1,114 @@
+using Microsoft.Extensions.Configuration;
+
+namespace Hj.ReverseProxy.Aspire.UnitTest;
+
+public class ServiceDiscoveryTests
+{
+ [Fact]
+ public void DiscoverEndpointList_GivenInvalidQuery_ReturnsEmpty()
+ {
+ // arrange
+ var config = GetConfiguration([]);
+
+ // act & assert
+ Assert.Throws(() => ServiceDiscovery.DiscoverEndpointList(config, "invalid-query"));
+ }
+
+ [Fact]
+ public void DiscoverEndpointList_GivenMissingServiceDiscoverySection_ReturnsEmpty()
+ {
+ // arrange
+ var config = GetConfiguration([]);
+
+ // act
+ var result = ServiceDiscovery.DiscoverEndpointList(config, "https://service");
+
+ // assert
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void DiscoverEndpointList_GivenService_ReturnsEndpoints()
+ {
+ // arrange
+ var config = GetConfiguration(new Dictionary()
+ {
+ { "Services:Service:Endpoint:0", "http://localhost" },
+ { "Services:Service:Endpoint:1", "https://localhost" },
+ });
+
+ // act
+ var result = ServiceDiscovery.DiscoverEndpointList(config, "endpoint://service");
+
+ // assert
+ Assert.Equal("http://localhost", result[0]);
+ Assert.Equal("https://localhost", result[1]);
+ }
+
+ [Fact]
+ public void DiscoverEndpointList_GivenServiceAndNamedEndpoint_ReturnsNamedEndpoint()
+ {
+ // arrange
+ var config = GetConfiguration(new Dictionary()
+ {
+ { "Services:Service:Endpoint:0", "https://localhost" },
+ { "Services:Service:Named-Endpoint:0", "https://named-endpoint" },
+ });
+
+ // act
+ var result = ServiceDiscovery.DiscoverEndpointList(config, "https://_named-endpoint.service");
+
+ // assert
+ var item = Assert.Single(result);
+ Assert.Equal("https://named-endpoint", item);
+ }
+
+ [Theory]
+ [InlineData("http")]
+ [InlineData("https")]
+ public void DiscoverEndpointList_GivenServiceWithAllowedSchemes_ReturnsEndpointWithAllowedScheme(string allowedScheme)
+ {
+ // arrange
+ var config = GetConfiguration(new Dictionary()
+ {
+ { "Services:Service:Endpoint:0", "http://localhost" },
+ { "Services:Service:Endpoint:1", "https://localhost" },
+ });
+
+ string[] allowedSchemes = [allowedScheme];
+
+ // act
+ var result = ServiceDiscovery.DiscoverEndpointList(config, "http+https://_endpoint.service", allowedSchemes, false);
+
+ // assert
+ var item = Assert.Single(result);
+ Assert.Equal(allowedScheme + "://localhost", item);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("http+")]
+ public void DiscoverEndpointList_GivenInvalidAllowedSchemes_SkipsAllowedSchemeReturnsNone(string allowedScheme)
+ {
+ // arrange
+ var config = GetConfiguration(new Dictionary()
+ {
+ { "Services:Service:Endpoint:0", "http://localhost" },
+ { "Services:Service:Endpoint:1", "https://localhost" },
+ });
+
+ string[]? allowedSchemes = [allowedScheme];
+
+ // act
+ var result = ServiceDiscovery.DiscoverEndpointList(config, "http+https://_endpoint.service", allowedSchemes, false);
+
+ // assert
+ Assert.Empty(result);
+ }
+
+ private static IConfiguration GetConfiguration(Dictionary settings)
+ {
+ var initialData = settings.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value));
+ return new ConfigurationBuilder().AddInMemoryCollection(initialData).Build();
+ }
+}
diff --git a/test/ReverseProxy.UnitTest/ReverseProxy.UnitTest.csproj b/test/ReverseProxy.UnitTest/ReverseProxy.UnitTest.csproj
index 3b7bf6a..487c38b 100644
--- a/test/ReverseProxy.UnitTest/ReverseProxy.UnitTest.csproj
+++ b/test/ReverseProxy.UnitTest/ReverseProxy.UnitTest.csproj
@@ -14,12 +14,18 @@
-
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
-
-
-
-
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+