diff --git a/src/Ocelot/DependencyInjection/IOcelotBuilder.cs b/src/Ocelot/DependencyInjection/IOcelotBuilder.cs index 67585edd..13ec5a6a 100644 --- a/src/Ocelot/DependencyInjection/IOcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/IOcelotBuilder.cs @@ -24,5 +24,7 @@ namespace Ocelot.DependencyInjection IOcelotBuilder AddTransientDefinedAggregator() where T : class, IDefinedAggregator; + + IOcelotBuilder AddConfigPlaceholders(); } } diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 3f9d3fb5..6733b54c 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -37,6 +37,7 @@ namespace Ocelot.DependencyInjection using Ocelot.Security.IPSecurity; using Ocelot.ServiceDiscovery; using System; + using System.Linq; using System.Net.Http; using System.Reflection; @@ -209,5 +210,39 @@ namespace Ocelot.DependencyInjection return this; } + + public IOcelotBuilder AddConfigPlaceholders() + { + // see: https://greatrexpectations.com/2018/10/25/decorators-in-net-core-with-dependency-injection + var wrappedDescriptor = Services.First(x => x.ServiceType == typeof(IPlaceholders)); + + var objectFactory = ActivatorUtilities.CreateFactory( + typeof(ConfigAwarePlaceholders), + new[] { typeof(IPlaceholders) }); + + Services.Replace(ServiceDescriptor.Describe( + typeof(IPlaceholders), + s => (IPlaceholders) objectFactory(s, + new[] {CreateInstance(s, wrappedDescriptor)}), + wrappedDescriptor.Lifetime + )); + + return this; + } + + private static object CreateInstance(IServiceProvider services, ServiceDescriptor descriptor) + { + if (descriptor.ImplementationInstance != null) + { + return descriptor.ImplementationInstance; + } + + if (descriptor.ImplementationFactory != null) + { + return descriptor.ImplementationFactory(services); + } + + return ActivatorUtilities.GetServiceOrCreateInstance(services, descriptor.ImplementationType); + } } } diff --git a/src/Ocelot/Infrastructure/ConfigAwarePlaceholders.cs b/src/Ocelot/Infrastructure/ConfigAwarePlaceholders.cs new file mode 100644 index 00000000..254dd4a5 --- /dev/null +++ b/src/Ocelot/Infrastructure/ConfigAwarePlaceholders.cs @@ -0,0 +1,61 @@ +namespace Ocelot.Infrastructure +{ + using System; + using System.Text.RegularExpressions; + using Microsoft.Extensions.Configuration; + using Request.Middleware; + using Responses; + + public class ConfigAwarePlaceholders : IPlaceholders + { + private readonly IConfiguration _configuration; + private readonly IPlaceholders _placeholders; + + public ConfigAwarePlaceholders(IConfiguration configuration, IPlaceholders placeholders) + { + _configuration = configuration; + _placeholders = placeholders; + } + + public Response Get(string key) + { + var placeholderResponse = _placeholders.Get(key); + + if (!placeholderResponse.IsError) + { + return placeholderResponse; + } + + return GetFromConfig(CleanKey(key)); + } + + public Response Get(string key, DownstreamRequest request) + { + var placeholderResponse = _placeholders.Get(key, request); + + if (!placeholderResponse.IsError) + { + return placeholderResponse; + } + + return GetFromConfig(CleanKey(key)); + } + + public Response Add(string key, Func> func) + => _placeholders.Add(key, func); + + public Response Remove(string key) + => _placeholders.Remove(key); + + private string CleanKey(string key) + => Regex.Replace(key, @"[{}]", string.Empty, RegexOptions.None); + + private Response GetFromConfig(string key) + { + var valueFromConfig = _configuration[key]; + return valueFromConfig == null + ? (Response) new ErrorResponse(new CouldNotFindPlaceholderError(key)) + : new OkResponse(valueFromConfig); + } + } +} diff --git a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs index b46649f8..3ec06a8b 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs @@ -14,6 +14,7 @@ namespace Ocelot.UnitTests.DependencyInjection using System.Collections.Generic; using System.Linq; using System.Net.Http; + using Ocelot.Infrastructure; using TestStack.BDDfy; using Xunit; using static Ocelot.UnitTests.Middleware.UserDefinedResponseAggregatorTests; @@ -136,6 +137,16 @@ namespace Ocelot.UnitTests.DependencyInjection .BDDfy(); } + [Fact] + public void should_replace_iplaceholder() + { + this.Given(x => x.WhenISetUpOcelotServices()) + .When(x => AddConfigPlaceholders()) + .Then(x => ThenAnExceptionIsntThrown()) + .And(x => ThenTheIPlaceholderInstanceIsReplaced()) + .BDDfy(); + } + private void AddSingletonDefinedAggregator() where T : class, IDefinedAggregator { @@ -148,6 +159,11 @@ namespace Ocelot.UnitTests.DependencyInjection _ocelotBuilder.AddTransientDefinedAggregator(); } + private void AddConfigPlaceholders() + { + _ocelotBuilder.AddConfigPlaceholders(); + } + private void ThenTheSpecificHandlersAreTransient() { var handlers = _serviceProvider.GetServices().ToList(); @@ -235,6 +251,13 @@ namespace Ocelot.UnitTests.DependencyInjection _ocelotBuilder.ShouldBeOfType(); } + private void ThenTheIPlaceholderInstanceIsReplaced() + { + _serviceProvider = _services.BuildServiceProvider(); + var placeholders = _serviceProvider.GetService(); + placeholders.ShouldBeOfType(); + } + private void WhenISetUpOcelotServices() { try diff --git a/test/Ocelot.UnitTests/Infrastructure/ConfigAwarePlaceholdersTests.cs b/test/Ocelot.UnitTests/Infrastructure/ConfigAwarePlaceholdersTests.cs new file mode 100644 index 00000000..ed88fc9d --- /dev/null +++ b/test/Ocelot.UnitTests/Infrastructure/ConfigAwarePlaceholdersTests.cs @@ -0,0 +1,78 @@ +namespace Ocelot.UnitTests.Infrastructure +{ + using System; + using Moq; + using Ocelot.Infrastructure; + using Responses; + using Shouldly; + using Microsoft.Extensions.Configuration; + using Xunit; + + public class ConfigAwarePlaceholdersTests + { + private readonly IPlaceholders _placeholders; + private readonly Mock _basePlaceholders; + + public ConfigAwarePlaceholdersTests() + { + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddJsonFile("appsettings.json"); + var configuration = configurationBuilder.Build(); + + _basePlaceholders = new Mock(); + _placeholders = new ConfigAwarePlaceholders(configuration, _basePlaceholders.Object); + } + + [Fact] + public void should_return_value_from_underlying_placeholders() + { + var baseUrl = "http://www.bbc.co.uk"; + const string key = "{BaseUrl}"; + + _basePlaceholders.Setup(x => x.Get(key)).Returns(new OkResponse(baseUrl)); + var result = _placeholders.Get(key); + result.Data.ShouldBe(baseUrl); + } + + [Fact] + public void should_return_value_from_config_with_same_name_as_placeholder_if_underlying_placeholder_not_found() + { + const string expected = "http://foo-bar.co.uk"; + var baseUrl = "http://www.bbc.co.uk"; + const string key = "{BaseUrl}"; + + _basePlaceholders.Setup(x => x.Get(key)).Returns(new ErrorResponse(new FakeError())); + var result = _placeholders.Get(key); + result.Data.ShouldBe(expected); + } + + [Theory] + [InlineData("{TestConfig}")] + [InlineData("{TestConfigNested:Child}")] + public void should_return_value_from_config(string key) + { + const string expected = "foo"; + + _basePlaceholders.Setup(x => x.Get(key)).Returns(new ErrorResponse(new FakeError())); + var result = _placeholders.Get(key); + result.Data.ShouldBe(expected); + } + + [Fact] + public void should_call_underyling_when_added() + { + const string key = "{Test}"; + Func> func = () => new OkResponse("test)"); + _placeholders.Add(key, func); + _basePlaceholders.Verify(p => p.Add(key, func), Times.Once); + } + + [Fact] + public void should_call_underyling_when_removed() + { + const string key = "{Test}"; + _placeholders.Remove(key); + _basePlaceholders.Verify(p => p.Remove(key), Times.Once); + } + } +} diff --git a/test/Ocelot.UnitTests/appsettings.json b/test/Ocelot.UnitTests/appsettings.json index 247f1ad7..57566b4e 100644 --- a/test/Ocelot.UnitTests/appsettings.json +++ b/test/Ocelot.UnitTests/appsettings.json @@ -20,5 +20,10 @@ "port": 5000, "hostName": "localhost" } + }, + "BaseUrl": "http://foo-bar.co.uk", + "TestConfig": "foo", + "TestConfigNested":{ + "Child": "foo" } }