diff --git a/src/CodeConfigSample/CCProxy/CCProxy.csproj b/src/CodeConfigSample/CCProxy/CCProxy.csproj
new file mode 100644
index 0000000..1a21507
--- /dev/null
+++ b/src/CodeConfigSample/CCProxy/CCProxy.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net5.0
+
+
+
+
+
+
+
+
+
diff --git a/src/CodeConfigSample/CCProxy/ConsulMonitorWorker.cs b/src/CodeConfigSample/CCProxy/ConsulMonitorWorker.cs
new file mode 100644
index 0000000..019ee9d
--- /dev/null
+++ b/src/CodeConfigSample/CCProxy/ConsulMonitorWorker.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using Consul;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.ReverseProxy.Abstractions;
+using Microsoft.ReverseProxy.Service;
+
+namespace CCProxy
+{
+ public class ConsulMonitorWorker : BackgroundService
+ {
+ private readonly IConsulClient _consulClient;
+ private readonly IProxyConfigProvider _proxyConfigProvider;
+ private readonly IConfigValidator _proxyConfigValidator;
+ private readonly ILogger _logger;
+ private const int DEFAULT_CONSUL_POLL_INTERVAL_MINS = 2;
+
+ public ConsulMonitorWorker(IConsulClient consulClient, IProxyConfigProvider proxyConfigProvider,
+ IConfigValidator proxyConfigValidator, ILogger logger)
+ {
+ _consulClient = consulClient;
+ _proxyConfigProvider = proxyConfigProvider;
+ _proxyConfigValidator = proxyConfigValidator;
+ _logger = logger;
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ var serviceResult = await _consulClient.Agent.Services(stoppingToken);
+
+ if (serviceResult.StatusCode == HttpStatusCode.OK)
+ {
+ var clusters = await ExtractClusters(serviceResult);
+ var routes = await ExtractRoutes(serviceResult);
+
+ (_proxyConfigProvider as ConsulProxyConfigProvider)?.Update(routes, clusters);
+ }
+
+ await Task.Delay(TimeSpan.FromMinutes(DEFAULT_CONSUL_POLL_INTERVAL_MINS), stoppingToken);
+ }
+ }
+
+ private async Task> ExtractClusters(QueryResult> serviceResult)
+ {
+ var clusters = new Dictionary();
+ var serviceMapping = serviceResult.Response;
+ foreach (var (key, svc) in serviceMapping)
+ {
+ var cluster = clusters.ContainsKey(svc.Service)
+ ? clusters[svc.Service]
+ : new Cluster {Id = svc.Service};
+
+ cluster.Destinations.Add(svc.ID, new Destination {Address = $"{svc.Address}:{svc.Port}"});
+
+ var clusterErrs = await _proxyConfigValidator.ValidateClusterAsync(cluster);
+ if (clusterErrs.Any())
+ {
+ _logger.LogError("Errors found when creating clusters for {Service}", svc.Service);
+ clusterErrs.ForEach(err => _logger.LogError(err, $"{svc.Service} cluster validation error"));
+ continue;
+ }
+
+ clusters[svc.Service] = cluster;
+ }
+
+ return clusters.Values.ToList();
+ }
+
+ private async Task> ExtractRoutes(QueryResult> serviceResult)
+ {
+ var serviceMapping = serviceResult.Response;
+ List routes = new List();
+ foreach (var (key, svc) in serviceMapping)
+ {
+ if (svc.Meta.TryGetValue("yarp", out string enableYarp) &&
+ enableYarp.Equals("on", StringComparison.InvariantCulture))
+ {
+ if (routes.Any(r => r.ClusterId == svc.Service)) continue;
+
+ ProxyRoute route = new ProxyRoute
+ {
+ ClusterId = svc.Service,
+ RouteId = $"{svc.Service}-route",
+ Match =
+ {
+ Path = svc.Meta.ContainsKey("yarp_path")?svc.Meta["yarp_path"] : default,
+ Hosts = svc.Meta.ContainsKey("yarp_hosts")? svc.Meta["yarp_hosts"].Split(',') : default
+ }
+ };
+
+ var routeErrs = await _proxyConfigValidator.ValidateRouteAsync(route);
+ if (routeErrs.Any())
+ {
+ _logger.LogError("Errors found when trying to generate routes for {Service}", svc.Service);
+ routeErrs.ForEach(err => _logger.LogError(err, $"{svc.Service} route validation error"));
+ continue;
+ }
+ routes.Add(route);
+ }
+ }
+ return routes;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CodeConfigSample/CCProxy/ConsulProxyConfigProvider.cs b/src/CodeConfigSample/CCProxy/ConsulProxyConfigProvider.cs
new file mode 100644
index 0000000..8b4d131
--- /dev/null
+++ b/src/CodeConfigSample/CCProxy/ConsulProxyConfigProvider.cs
@@ -0,0 +1,50 @@
+using System.Collections.Generic;
+using System.Threading;
+using Consul;
+using Microsoft.Extensions.Primitives;
+using Microsoft.ReverseProxy.Abstractions;
+using Microsoft.ReverseProxy.Service;
+
+namespace CCProxy
+{
+ public class ConsulProxyConfigProvider : IProxyConfigProvider
+ {
+ private readonly IConsulClient _consulClient;
+ private volatile ConsulProxyConfig _config;
+
+ public ConsulProxyConfigProvider(IConsulClient consulClient)
+ {
+ _consulClient = consulClient;
+ _config = new ConsulProxyConfig(null, null);
+ }
+
+ public IProxyConfig GetConfig() => _config;
+
+ public virtual void Update(IReadOnlyList routes, IReadOnlyList clusters)
+ {
+ var oldConfig = _config;
+ _config = new ConsulProxyConfig(routes, clusters);
+ oldConfig.SignalChange();
+ }
+
+ private class ConsulProxyConfig : IProxyConfig
+ {
+ private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+ public IReadOnlyList Routes { get; }
+ public IReadOnlyList Clusters { get; }
+ public IChangeToken ChangeToken { get; }
+
+ public ConsulProxyConfig(IReadOnlyList routes, IReadOnlyList clusters)
+ {
+ Routes = routes;
+ Clusters = clusters;
+ ChangeToken = new CancellationChangeToken(_cts.Token);
+ }
+
+ internal void SignalChange()
+ {
+ _cts.Cancel();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CodeConfigSample/CCProxy/Controllers/YarpController.cs b/src/CodeConfigSample/CCProxy/Controllers/YarpController.cs
new file mode 100644
index 0000000..3caffb4
--- /dev/null
+++ b/src/CodeConfigSample/CCProxy/Controllers/YarpController.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Linq;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.ReverseProxy.Service;
+
+namespace CCProxy.Controllers
+{
+ [Route("/yarp")]
+ [ApiController]
+ public class YarpController : Controller
+ {
+ private readonly IProxyConfigProvider _proxyConfigProvider;
+
+ public YarpController(IProxyConfigProvider proxyConfigProvider)
+ {
+ _proxyConfigProvider = proxyConfigProvider;
+ }
+
+ [HttpGet("routes")]
+ public ActionResult DumpRoutes()
+ {
+ var proxyConfig = _proxyConfigProvider.GetConfig();
+ return Ok(proxyConfig.Routes);
+ }
+
+ [HttpGet("clusters")]
+ public ActionResult DumpClusters()
+ {
+ var proxyConfig = _proxyConfigProvider.GetConfig();
+ return Ok(proxyConfig.Clusters);
+ }
+
+ [HttpGet("incoming")]
+ public IActionResult Dump()
+ {
+ var result = new
+ {
+ Request.Method,
+ Request.Protocol,
+ Request.Scheme,
+ Host = Request.Host.Value,
+ PathBase = Request.PathBase.Value,
+ Path = Request.Path.Value,
+ Query = Request.QueryString.Value,
+ Headers = Request.Headers.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToArray()),
+ Time = DateTimeOffset.UtcNow
+ };
+
+ return Ok(result);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CodeConfigSample/CCProxy/Dockerfile b/src/CodeConfigSample/CCProxy/Dockerfile
new file mode 100644
index 0000000..33f23f7
--- /dev/null
+++ b/src/CodeConfigSample/CCProxy/Dockerfile
@@ -0,0 +1,19 @@
+FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
+WORKDIR /app
+EXPOSE 80
+
+FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
+WORKDIR /src
+COPY ["CCProxy.csproj", "./"]
+RUN dotnet restore "CCProxy.csproj"
+COPY . .
+WORKDIR "/src"
+RUN dotnet build "CCProxy.csproj" -c Release -o /app/build
+
+FROM build AS publish
+RUN dotnet publish "CCProxy.csproj" -c Release -o /app/publish
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "CCProxy.dll"]
diff --git a/src/CodeConfigSample/CCProxy/Middleware/HttpInspectorMiddleware.cs b/src/CodeConfigSample/CCProxy/Middleware/HttpInspectorMiddleware.cs
new file mode 100644
index 0000000..b23dd34
--- /dev/null
+++ b/src/CodeConfigSample/CCProxy/Middleware/HttpInspectorMiddleware.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Data.Common;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.ReverseProxy.Middleware;
+
+namespace CCProxy.Middleware
+{
+ public class HttpInspectorMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly ILogger _logger;
+
+ public HttpInspectorMiddleware(RequestDelegate next,
+ ILogger logger)
+ {
+ _next = next;
+ _logger = logger;
+ }
+
+ public async Task Invoke(HttpContext context)
+ {
+ LogRequest(context);
+ LogDestinations(context);
+ await _next(context);
+ }
+
+ private void LogDestinations(HttpContext context)
+ {
+ _logger.LogDebug("Available Destinations: ");
+ var proxyFeature = context.Features.Get();
+
+ proxyFeature.AvailableDestinations.ForEach(dest =>
+ {
+ _logger.LogDebug("Destination {ID} {Address}", dest.DestinationId, dest.Config.Address);
+ });
+ }
+
+ private void LogRequest(HttpContext context)
+ {
+ var buffer = new StringBuilder();
+
+ context.Request.Headers
+ .ForEach(kvp => buffer.Append($"{kvp.Key}: {kvp.Value}{Environment.NewLine}"));
+
+ _logger.LogDebug($"Http Request Information:{Environment.NewLine}" +
+ $"Schema: {context.Request.Scheme}{Environment.NewLine}" +
+ $"Host: {context.Request.Host}{Environment.NewLine}" +
+ $"Path: {context.Request.Path}{Environment.NewLine}" +
+ $"QueryString: {context.Request.QueryString}{Environment.NewLine}" +
+ $"Headers: {Environment.NewLine}{buffer}{Environment.NewLine}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CodeConfigSample/CCProxy/Program.cs b/src/CodeConfigSample/CCProxy/Program.cs
new file mode 100644
index 0000000..fe0db69
--- /dev/null
+++ b/src/CodeConfigSample/CCProxy/Program.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace CCProxy
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ CreateHostBuilder(args).Build().Run();
+ }
+
+ public static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseStartup();
+ });
+ }
+}
diff --git a/src/CodeConfigSample/CCProxy/Properties/launchSettings.json b/src/CodeConfigSample/CCProxy/Properties/launchSettings.json
new file mode 100644
index 0000000..d8b549b
--- /dev/null
+++ b/src/CodeConfigSample/CCProxy/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "CCProxy": {
+ "commandName": "Project",
+ "dotnetRunMessages": "true",
+ "applicationUrl": "http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "ASPNETCORE_URLS" : "http://0.0.0.0:5000"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CodeConfigSample/CCProxy/ServiceCollectionExtensions.cs b/src/CodeConfigSample/CCProxy/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..61e0164
--- /dev/null
+++ b/src/CodeConfigSample/CCProxy/ServiceCollectionExtensions.cs
@@ -0,0 +1,31 @@
+using System;
+using Consul;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace CCProxy
+{
+ public static class ServiceCollectionExtensions
+ {
+ public static IServiceCollection AddConsulClient(this IServiceCollection services)
+ {
+ return services.AddConsulClient(options => { });
+ }
+
+ public static IServiceCollection AddConsulClient(
+ this IServiceCollection services,
+ Action options)
+ {
+ /*
+ * CONSUL_HTTP_ADDR
+ * CONSUL_HTTP_SSL
+ * CONSUL_HTTP_SSL_VERIFY
+ * CONSUL_HTTP_AUTH
+ * CONSUL_HTTP_TOKEN
+ */
+ services.TryAddSingleton(sp => new ConsulClient(options));
+
+ return services;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CodeConfigSample/CCProxy/Startup.cs b/src/CodeConfigSample/CCProxy/Startup.cs
new file mode 100644
index 0000000..b5276c7
--- /dev/null
+++ b/src/CodeConfigSample/CCProxy/Startup.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+using CCProxy.Middleware;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.ReverseProxy.Service;
+
+namespace CCProxy
+{
+ public class Startup
+ {
+ public Startup(IConfiguration configuration)
+ {
+ Configuration = configuration;
+ }
+
+ public IConfiguration Configuration { get; }
+
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddSingleton();
+
+ services.AddReverseProxy();
+
+ services.AddHealthChecks();
+ services.AddControllers()
+ .AddJsonOptions(option =>
+ {
+ option.JsonSerializerOptions.IgnoreNullValues = true;
+ option.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+ });
+
+ services.AddConsulClient(opts =>
+ {
+ var consulClientConfig = Configuration.GetSection("consul:client");
+ var port = consulClientConfig.GetValue("port");
+ var host = consulClientConfig.GetValue("host");
+ var scheme = consulClientConfig.GetValue("scheme");
+ var dc = consulClientConfig.GetValue("datacenter");
+
+ var address = $"{scheme}://{host}:{port}";
+ opts.Address = new Uri(address);
+ opts.Datacenter = dc;
+ });
+
+ services.AddHostedService();
+ }
+
+
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ app.UseRouting();
+
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapHealthChecks("/health");
+ endpoints.MapReverseProxy(pipeline => pipeline.UseMiddleware());
+ endpoints.MapControllers();
+
+ endpoints.MapGet("/", async context =>
+ {
+ await context.Response.WriteAsync("Hello World!");
+ });
+ });
+ }
+ }
+}
diff --git a/src/CodeConfigSample/CCProxy/appsettings.Development.json b/src/CodeConfigSample/CCProxy/appsettings.Development.json
new file mode 100644
index 0000000..d33fbc8
--- /dev/null
+++ b/src/CodeConfigSample/CCProxy/appsettings.Development.json
@@ -0,0 +1,17 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "consul": {
+ "client": {
+ "host": "localhost",
+ "port": 8500,
+ "scheme": "http",
+ "datacenter" : "yarp_dc"
+ }
+ }
+}
diff --git a/src/CodeConfigSample/CCProxy/appsettings.Docker.json b/src/CodeConfigSample/CCProxy/appsettings.Docker.json
new file mode 100644
index 0000000..1f09ea4
--- /dev/null
+++ b/src/CodeConfigSample/CCProxy/appsettings.Docker.json
@@ -0,0 +1,17 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "consul": {
+ "client": {
+ "host": "consul_service",
+ "port": 8500,
+ "scheme": "http",
+ "datacenter" : "yarp_dc"
+ }
+ }
+}
diff --git a/src/CodeConfigSample/CCProxy/appsettings.json b/src/CodeConfigSample/CCProxy/appsettings.json
new file mode 100644
index 0000000..d9d9a9b
--- /dev/null
+++ b/src/CodeConfigSample/CCProxy/appsettings.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/src/CodeConfigSample/CCProxy/proxyrequests.http b/src/CodeConfigSample/CCProxy/proxyrequests.http
new file mode 100644
index 0000000..7e3c407
--- /dev/null
+++ b/src/CodeConfigSample/CCProxy/proxyrequests.http
@@ -0,0 +1,14 @@
+### Cluters
+GET http://0.0.0.0:5000/yarp/clusters
+Accept: application/json
+
+### Routes
+GET http://0.0.0.0:5000/yarp/incoming
+Accept: application/json
+
+### Proxied Items
+GET http://0.0.0.0:5000/api/items
+Accept: application/json
+Host: items
+
+###
\ No newline at end of file