Feature/store configuraton json idented (#328)

* messing around with benchmark.net.seems Ocelot adds about 2ms to a request..lets make this less? :)

* #326 store json indented so it looks nice :P
pull/331/head
Tom Pallister 7 years ago committed by GitHub
parent 636d116491
commit 77211e9f16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -163,7 +163,7 @@ requests. This would also mean that subsequent requests dont use the cookies fro
UseCookieContainer to true unless you have a really really good reason. Just look at your response headers and forward the cookies back with your next request!
SSL Errors
----------
^^^^^^^^^^
Id you want to ignore SSL warnings / errors set the following in your ReRoute config.

@ -69,18 +69,20 @@ In order to get this working add the following to ocelot.json..
"Type": "Eureka"
}
And following the guide `Here <https://steeltoe.io/docs/steeltoe-discovery/>`_ you may also need to add some stuff to appsettings.json. For example the json below
tells the steeltoe / pivotal services where to look for the service discovery server and if the service should register with it.
And following the guide `Here <https://steeltoe.io/docs/steeltoe-discovery/>`_ you may also need to add some stuff to appsettings.json. For example the json below tells the steeltoe / pivotal services where to look for the service discovery server and if the service should register with it.
.. code-block:: json
"eureka": {
"client": {
"serviceUrl": "http://localhost:8761/eureka/",
"shouldRegisterWithEureka": true
"shouldRegisterWithEureka": false,
"shouldFetchRegistry": true
}
}
I am told that if shouldRegisterWithEureka is false then shouldFetchRegistry will defaut to true so you don't need it explicitly but left it in there.
Ocelot will now register all the necessary services when it starts up and if you have the json above will register itself with
Eureka. One of the services polls Eureka every 30 seconds (default) and gets the latest service state and persists this in memory.
When Ocelot asks for a given service it is retrieved from memory so performance is not a big problem. Please note that this code

@ -13,7 +13,7 @@ namespace Ocelot.Configuration.Repository
public class ConsulFileConfigurationRepository : IFileConfigurationRepository
{
private readonly ConsulClient _consul;
private readonly IConsulClient _consul;
private const string OcelotConfiguration = "InternalConfiguration";
private readonly Cache.IOcelotCache<FileConfiguration> _cache;
private readonly IOcelotLogger _logger;
@ -72,7 +72,7 @@ namespace Ocelot.Configuration.Repository
public async Task<Response> Set(FileConfiguration ocelotConfiguration)
{
var json = JsonConvert.SerializeObject(ocelotConfiguration);
var json = JsonConvert.SerializeObject(ocelotConfiguration, Formatting.Indented);
var bytes = Encoding.UTF8.GetBytes(json);

@ -36,7 +36,7 @@ namespace Ocelot.Configuration.Repository
public Task<Response> Set(FileConfiguration fileConfiguration)
{
string jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration);
string jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented);
lock(_lock)
{

@ -6,7 +6,7 @@ namespace Ocelot.Infrastructure.Consul
{
public class ConsulClientFactory : IConsulClientFactory
{
public ConsulClient Get(ConsulRegistryConfiguration config)
public IConsulClient Get(ConsulRegistryConfiguration config)
{
return new ConsulClient(c =>
{

@ -5,6 +5,6 @@ namespace Ocelot.Infrastructure.Consul
{
public interface IConsulClientFactory
{
ConsulClient Get(ConsulRegistryConfiguration config);
IConsulClient Get(ConsulRegistryConfiguration config);
}
}

@ -15,7 +15,7 @@ namespace Ocelot.ServiceDiscovery.Providers
{
private readonly ConsulRegistryConfiguration _config;
private readonly IOcelotLogger _logger;
private readonly ConsulClient _consul;
private readonly IConsulClient _consul;
private const string VersionPrefix = "version-";
public ConsulServiceDiscoveryProvider(ConsulRegistryConfiguration config, IOcelotLoggerFactory factory, IConsulClientFactory clientFactory)

@ -88,7 +88,7 @@ namespace Ocelot.AcceptanceTests
{
var configurationPath = TestConfiguration.ConfigurationPath;
var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration);
var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented);
if (File.Exists(configurationPath))
{
@ -100,7 +100,7 @@ namespace Ocelot.AcceptanceTests
public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration, string configurationPath)
{
var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration);
var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented);
if (File.Exists(configurationPath))
{

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.IO;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Ocelot.Configuration.File;
using Ocelot.DownstreamRouteFinder.UrlMatcher;
using Ocelot.Middleware;
using Ocelot.DependencyInjection;
using System.Net.Http;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes.Jobs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Validators;
namespace Ocelot.Benchmarks
{
[Config(typeof(AllTheThingsBenchmarks))]
public class AllTheThingsBenchmarks : ManualConfig
{
private IWebHost _service;
private IWebHost _ocelot;
private HttpClient _httpClient;
public AllTheThingsBenchmarks()
{
Add(StatisticColumn.AllStatistics);
Add(MemoryDiagnoser.Default);
Add(BaselineValidator.FailOnError);
}
[GlobalSetup]
public void SetUp()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 51879,
}
},
DownstreamScheme = "http",
UpstreamPathTemplate = "/",
UpstreamHttpMethod = new List<string> { "Get" },
}
}
};
GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 201, string.Empty);
GivenThereIsAConfiguration(configuration);
GivenOcelotIsRunning("http://localhost:5000");
_httpClient = new HttpClient();
}
[Benchmark(Baseline = true)]
public async Task Baseline()
{
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:5000/");
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
}
// * Summary *
// BenchmarkDotNet=v0.10.13, OS=macOS 10.12.6 (16G1212) [Darwin 16.7.0]
// Intel Core i5-4278U CPU 2.60GHz (Haswell), 1 CPU, 4 logical cores and 2 physical cores
// .NET Core SDK=2.1.4
// [Host] : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT
// DefaultJob : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT
// Method | Mean | Error | StdDev | StdErr | Min | Q1 | Median | Q3 | Max | Op/s | Scaled | Gen 0 | Gen 1 | Allocated |
// --------- |---------:|----------:|----------:|----------:|---------:|---------:|---------:|---------:|---------:|------:|-------:|--------:|-------:|----------:|
// Baseline | 2.102 ms | 0.0292 ms | 0.0273 ms | 0.0070 ms | 2.063 ms | 2.080 ms | 2.093 ms | 2.122 ms | 2.152 ms | 475.8 | 1.00 | 31.2500 | 3.9063 | 1.63 KB |
private void GivenOcelotIsRunning(string url)
{
_ocelot = new WebHostBuilder()
.UseKestrel()
.UseUrls(url)
.UseContentRoot(Directory.GetCurrentDirectory())
.ConfigureAppConfiguration((hostingContext, config) =>
{
config
.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath)
.AddJsonFile("appsettings.json", true, true)
.AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true)
.AddJsonFile("ocelot.json")
.AddEnvironmentVariables();
})
.ConfigureServices(s => {
s.AddOcelot();
})
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
})
.UseIISIntegration()
.Configure(app =>
{
app.UseOcelot().Wait();
})
.Build();
_ocelot.Start();
}
public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration)
{
var configurationPath = Path.Combine(AppContext.BaseDirectory, "ocelot.json");;
var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration);
if (File.Exists(configurationPath))
{
File.Delete(configurationPath);
}
File.WriteAllText(configurationPath, jsonConfiguration);
}
private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody)
{
_service = new WebHostBuilder()
.UseUrls(baseUrl)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.Configure(app =>
{
app.UsePathBase(basePath);
app.Run(async context =>
{
context.Response.StatusCode = statusCode;
await context.Response.WriteAsync(responseBody);
});
})
.Build();
_service.Start();
}
}
}

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.IO;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Ocelot.Configuration.File;
using Ocelot.DownstreamRouteFinder.UrlMatcher;
using Ocelot.Middleware;
using Ocelot.DependencyInjection;
using System.Net.Http;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes.Jobs;
using Ocelot.Configuration.Repository;
using Ocelot.Infrastructure.RequestData;
using Ocelot.Logging;
using Ocelot.Errors.Middleware;
using Microsoft.Extensions.DependencyInjection;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Validators;
namespace Ocelot.Benchmarks
{
[SimpleJob(launchCount: 1, warmupCount: 2, targetCount: 5)]
[Config(typeof(ExceptionHandlerMiddlewareBenchmarks))]
public class ExceptionHandlerMiddlewareBenchmarks : ManualConfig
{
private ExceptionHandlerMiddleware _middleware;
private DownstreamContext _downstreamContext;
private OcelotRequestDelegate _next;
public ExceptionHandlerMiddlewareBenchmarks()
{
Add(StatisticColumn.AllStatistics);
Add(MemoryDiagnoser.Default);
Add(BaselineValidator.FailOnError);
}
[GlobalSetup]
public void SetUp()
{
var serviceCollection = new ServiceCollection();
var config = new ConfigurationRoot(new List<IConfigurationProvider>());
var builder = new OcelotBuilder(serviceCollection, config);
var services = serviceCollection.BuildServiceProvider();
var loggerFactory = services.GetService<IOcelotLoggerFactory>();
var configRepo = services.GetService<IInternalConfigurationRepository>();
var repo = services.GetService<IRequestScopedDataRepository>();
_next = async context => {
await Task.CompletedTask;
throw new Exception("BOOM");
};
_middleware = new ExceptionHandlerMiddleware(_next, loggerFactory, configRepo, repo);
_downstreamContext = new DownstreamContext(new DefaultHttpContext());
}
[Benchmark(Baseline = true)]
public async Task Baseline()
{
await _middleware.Invoke(_downstreamContext);
}
}
}

@ -7,7 +7,9 @@ namespace Ocelot.Benchmarks
public static void Main(string[] args)
{
var switcher = new BenchmarkSwitcher(new[] {
typeof(UrlPathToUrlPathTemplateMatcherBenchmarks),
typeof(UrlPathToUrlPathTemplateMatcherBenchmarks),
typeof(AllTheThingsBenchmarks),
typeof(ExceptionHandlerMiddlewareBenchmarks)
});
switcher.Run(args);

@ -1,6 +1,9 @@
using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Validators;
using Ocelot.DownstreamRouteFinder.UrlMatcher;
namespace Ocelot.Benchmarks
@ -15,6 +18,8 @@ namespace Ocelot.Benchmarks
public UrlPathToUrlPathTemplateMatcherBenchmarks()
{
Add(StatisticColumn.AllStatistics);
Add(MemoryDiagnoser.Default);
Add(BaselineValidator.FailOnError);
}
[GlobalSetup]
@ -25,16 +30,23 @@ namespace Ocelot.Benchmarks
_downstreamUrlPathTemplate = "api/product/products/{productId}/variants/";
}
[Benchmark]
public void Benchmark1()
[Benchmark(Baseline = true)]
public void Baseline()
{
_urlPathMatcher.Match(_downstreamUrlPath, _downstreamUrlPathTemplate);
}
[Benchmark]
public void Benchmark2()
{
_urlPathMatcher.Match(_downstreamUrlPath, _downstreamUrlPathTemplate);
}
// * Summary *
// BenchmarkDotNet=v0.10.13, OS=macOS 10.12.6 (16G1212) [Darwin 16.7.0]
// Intel Core i5-4278U CPU 2.60GHz (Haswell), 1 CPU, 4 logical cores and 2 physical cores
// .NET Core SDK=2.1.4
// [Host] : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT
// DefaultJob : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT
// Method | Mean | Error | StdDev | StdErr | Min | Q1 | Median | Q3 | Max | Op/s |
// ----------- |---------:|----------:|----------:|----------:|---------:|---------:|---------:|---------:|---------:|----------:|
// Benchmark1 | 3.133 us | 0.0492 us | 0.0460 us | 0.0119 us | 3.082 us | 3.100 us | 3.122 us | 3.168 us | 3.233 us | 319,161.9 |
}
}

@ -0,0 +1,137 @@
namespace Ocelot.UnitTests.Configuration
{
using Xunit;
using TestStack.BDDfy;
using Shouldly;
using Ocelot.Configuration.Repository;
using Moq;
using Ocelot.Infrastructure.Consul;
using Ocelot.Logging;
using Ocelot.Configuration.File;
using Ocelot.Cache;
using System;
using System.Collections.Generic;
using Ocelot.Responses;
using System.Threading.Tasks;
using Ocelot.Configuration;
using Ocelot.Configuration.Builder;
using Ocelot.ServiceDiscovery.Configuration;
using Consul;
using Newtonsoft.Json;
using System.Text;
using System.Threading;
using System.Linq;
public class ConsulFileConfigurationRepositoryTests
{
private ConsulFileConfigurationRepository _repo;
private Mock<IOcelotCache<FileConfiguration>> _cache;
private Mock<IInternalConfigurationRepository> _internalRepo;
private Mock<IConsulClientFactory> _factory;
private Mock<IOcelotLoggerFactory> _loggerFactory;
private Mock<IConsulClient> _client;
private Mock<IKVEndpoint> _kvEndpoint;
private FileConfiguration _fileConfiguration;
private Response _result;
public ConsulFileConfigurationRepositoryTests()
{
_cache = new Mock<IOcelotCache<FileConfiguration>>();
_internalRepo = new Mock<IInternalConfigurationRepository>();
_loggerFactory = new Mock<IOcelotLoggerFactory>();
_factory = new Mock<IConsulClientFactory>();
_client = new Mock<IConsulClient>();
_kvEndpoint = new Mock<IKVEndpoint>();
_client
.Setup(x => x.KV)
.Returns(_kvEndpoint.Object);
_factory
.Setup(x => x.Get(It.IsAny<ConsulRegistryConfiguration>()))
.Returns(_client.Object);
_internalRepo
.Setup(x => x.Get())
.Returns(new OkResponse<IInternalConfiguration>(new InternalConfiguration(new List<ReRoute>(), "", new ServiceProviderConfigurationBuilder().Build(), "")));
_repo = new ConsulFileConfigurationRepository(_cache.Object, _internalRepo.Object, _factory.Object, _loggerFactory.Object);
}
[Fact]
public void should_set_config()
{
var config = FakeFileConfiguration();
this.Given(_ => GivenIHaveAConfiguration(config))
.And(_ => GivenWritingToConsulSucceeds())
.When(_ => WhenISetTheConfiguration())
.Then(_ => ThenTheConfigurationIsStoredAs(config))
.BDDfy();
}
private void GivenWritingToConsulSucceeds()
{
var response = new WriteResult<bool>();
response.Response = true;
_kvEndpoint
.Setup(x => x.Put(It.IsAny<KVPair>(), It.IsAny<CancellationToken>())).ReturnsAsync(response);
}
private void ThenTheConfigurationIsStoredAs(FileConfiguration config)
{
var json = JsonConvert.SerializeObject(config, Formatting.Indented);
var bytes = Encoding.UTF8.GetBytes(json);
_kvEndpoint
.Verify(x => x.Put(It.Is<KVPair>(k => k.Value.SequenceEqual(bytes)), It.IsAny<CancellationToken>()), Times.Once);
}
private async Task WhenISetTheConfiguration()
{
_result = await _repo.Set(_fileConfiguration);
}
private void GivenIHaveAConfiguration(FileConfiguration config)
{
_fileConfiguration = config;
}
private FileConfiguration FakeFileConfiguration()
{
var reRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "123.12.12.12",
Port = 80,
}
},
DownstreamScheme = "https",
DownstreamPathTemplate = "/asdfs/test/{test}"
}
};
var globalConfiguration = new FileGlobalConfiguration
{
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
{
Port = 198,
Host = "blah"
}
};
return new FileConfiguration
{
GlobalConfiguration = globalConfiguration,
ReRoutes = reRoutes
};
}
}
}

@ -1,21 +1,22 @@
using System;
using System.Collections.Generic;
using Moq;
using Ocelot.Configuration.File;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
using Newtonsoft.Json;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Ocelot.Configuration.Repository;
namespace Ocelot.UnitTests.Configuration
{
public class FileConfigurationRepositoryTests
using System;
using System.Collections.Generic;
using Moq;
using Ocelot.Configuration.File;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
using Newtonsoft.Json;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Ocelot.Configuration.Repository;
public class DiskFileConfigurationRepositoryTests
{
private readonly Mock<IHostingEnvironment> _hostingEnvironment = new Mock<IHostingEnvironment>();
private IFileConfigurationRepository _repo;
private string _configurationPath;
private FileConfiguration _result;
private FileConfiguration _fileConfiguration;
@ -24,7 +25,7 @@ namespace Ocelot.UnitTests.Configuration
// tests but whatever...
private string _environmentName = "DEV.DEV";
public FileConfigurationRepositoryTests()
public DiskFileConfigurationRepositoryTests()
{
_hostingEnvironment.Setup(he => he.EnvironmentName).Returns(_environmentName);
_repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object);
@ -35,9 +36,9 @@ namespace Ocelot.UnitTests.Configuration
{
var config = FakeFileConfigurationForGet();
this.Given(x => x.GivenTheConfigurationIs(config))
.When(x => x.WhenIGetTheReRoutes())
.Then(x => x.ThenTheFollowingIsReturned(config))
this.Given(_ => GivenTheConfigurationIs(config))
.When(_ => WhenIGetTheReRoutes())
.Then(_ => ThenTheFollowingIsReturned(config))
.BDDfy();
}
@ -46,10 +47,10 @@ namespace Ocelot.UnitTests.Configuration
{
var config = FakeFileConfigurationForGet();
this.Given(x => x.GivenTheEnvironmentNameIsUnavailable())
.And(x => x.GivenTheConfigurationIs(config))
.When(x => x.WhenIGetTheReRoutes())
.Then(x => x.ThenTheFollowingIsReturned(config))
this.Given(_ => GivenTheEnvironmentNameIsUnavailable())
.And(_ => GivenTheConfigurationIs(config))
.When(_ => WhenIGetTheReRoutes())
.Then(_ => ThenTheFollowingIsReturned(config))
.BDDfy();
}
@ -58,9 +59,10 @@ namespace Ocelot.UnitTests.Configuration
{
var config = FakeFileConfigurationForSet();
this.Given(x => GivenIHaveAConfiguration(config))
.When(x => WhenISetTheConfiguration())
.Then(x => ThenTheConfigurationIsStoredAs(config))
this.Given(_ => GivenIHaveAConfiguration(config))
.When(_ => WhenISetTheConfiguration())
.Then(_ => ThenTheConfigurationIsStoredAs(config))
.And(_ => ThenTheConfigurationJsonIsIndented(config))
.BDDfy();
}
@ -68,10 +70,12 @@ namespace Ocelot.UnitTests.Configuration
public void should_set_file_configuration_if_environment_name_is_unavailable()
{
var config = FakeFileConfigurationForSet();
this.Given(x => GivenIHaveAConfiguration(config))
.And(x => GivenTheEnvironmentNameIsUnavailable())
.When(x => WhenISetTheConfiguration())
.Then(x => ThenTheConfigurationIsStoredAs(config))
this.Given(_ => GivenIHaveAConfiguration(config))
.And(_ => GivenTheEnvironmentNameIsUnavailable())
.When(_ => WhenISetTheConfiguration())
.Then(_ => ThenTheConfigurationIsStoredAs(config))
.And(_ => ThenTheConfigurationJsonIsIndented(config))
.BDDfy();
}
@ -117,16 +121,25 @@ namespace Ocelot.UnitTests.Configuration
private void GivenTheConfigurationIs(FileConfiguration fileConfiguration)
{
var configurationPath = $"{AppContext.BaseDirectory}/ocelot{(string.IsNullOrEmpty(_environmentName) ? string.Empty : ".")}{_environmentName}.json";
_configurationPath = $"{AppContext.BaseDirectory}/ocelot{(string.IsNullOrEmpty(_environmentName) ? string.Empty : ".")}{_environmentName}.json";
var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration);
var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented);
if (File.Exists(configurationPath))
if (File.Exists(_configurationPath))
{
File.Delete(configurationPath);
File.Delete(_configurationPath);
}
File.WriteAllText(configurationPath, jsonConfiguration);
File.WriteAllText(_configurationPath, jsonConfiguration);
}
private void ThenTheConfigurationJsonIsIndented(FileConfiguration expecteds)
{
var path = !string.IsNullOrEmpty(_configurationPath) ? _configurationPath : _configurationPath = $"{AppContext.BaseDirectory}/ocelot{(string.IsNullOrEmpty(_environmentName) ? string.Empty : ".")}{_environmentName}.json";
var resultText = File.ReadAllText(_configurationPath);
var expectedText = JsonConvert.SerializeObject(expecteds, Formatting.Indented);
resultText.ShouldBe(expectedText);
}
private void WhenIGetTheReRoutes()
Loading…
Cancel
Save