Add ConfigAwarePlaceholders class (#997)

* Add ConfigAwarePlaceholders class

This allows placeholder values to be sourced from IConfiguration in
addition to set values.

* Rework how IPlaceholders is decorated in container
This commit is contained in:
donaldgray 2019-09-12 16:09:38 +01:00 committed by Thiago Loureiro
parent b6f3f0f28a
commit bef40041ba
6 changed files with 204 additions and 0 deletions

View File

@ -24,5 +24,7 @@ namespace Ocelot.DependencyInjection
IOcelotBuilder AddTransientDefinedAggregator<T>() IOcelotBuilder AddTransientDefinedAggregator<T>()
where T : class, IDefinedAggregator; where T : class, IDefinedAggregator;
IOcelotBuilder AddConfigPlaceholders();
} }
} }

View File

@ -37,6 +37,7 @@ namespace Ocelot.DependencyInjection
using Ocelot.Security.IPSecurity; using Ocelot.Security.IPSecurity;
using Ocelot.ServiceDiscovery; using Ocelot.ServiceDiscovery;
using System; using System;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Reflection; using System.Reflection;
@ -209,5 +210,39 @@ namespace Ocelot.DependencyInjection
return this; 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);
}
} }
} }

View File

@ -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<string> Get(string key)
{
var placeholderResponse = _placeholders.Get(key);
if (!placeholderResponse.IsError)
{
return placeholderResponse;
}
return GetFromConfig(CleanKey(key));
}
public Response<string> 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<Response<string>> 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<string> GetFromConfig(string key)
{
var valueFromConfig = _configuration[key];
return valueFromConfig == null
? (Response<string>) new ErrorResponse<string>(new CouldNotFindPlaceholderError(key))
: new OkResponse<string>(valueFromConfig);
}
}
}

View File

@ -14,6 +14,7 @@ namespace Ocelot.UnitTests.DependencyInjection
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using Ocelot.Infrastructure;
using TestStack.BDDfy; using TestStack.BDDfy;
using Xunit; using Xunit;
using static Ocelot.UnitTests.Middleware.UserDefinedResponseAggregatorTests; using static Ocelot.UnitTests.Middleware.UserDefinedResponseAggregatorTests;
@ -136,6 +137,16 @@ namespace Ocelot.UnitTests.DependencyInjection
.BDDfy(); .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<T>() private void AddSingletonDefinedAggregator<T>()
where T : class, IDefinedAggregator where T : class, IDefinedAggregator
{ {
@ -148,6 +159,11 @@ namespace Ocelot.UnitTests.DependencyInjection
_ocelotBuilder.AddTransientDefinedAggregator<T>(); _ocelotBuilder.AddTransientDefinedAggregator<T>();
} }
private void AddConfigPlaceholders()
{
_ocelotBuilder.AddConfigPlaceholders();
}
private void ThenTheSpecificHandlersAreTransient() private void ThenTheSpecificHandlersAreTransient()
{ {
var handlers = _serviceProvider.GetServices<DelegatingHandler>().ToList(); var handlers = _serviceProvider.GetServices<DelegatingHandler>().ToList();
@ -235,6 +251,13 @@ namespace Ocelot.UnitTests.DependencyInjection
_ocelotBuilder.ShouldBeOfType<OcelotBuilder>(); _ocelotBuilder.ShouldBeOfType<OcelotBuilder>();
} }
private void ThenTheIPlaceholderInstanceIsReplaced()
{
_serviceProvider = _services.BuildServiceProvider();
var placeholders = _serviceProvider.GetService<IPlaceholders>();
placeholders.ShouldBeOfType<ConfigAwarePlaceholders>();
}
private void WhenISetUpOcelotServices() private void WhenISetUpOcelotServices()
{ {
try try

View File

@ -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<IPlaceholders> _basePlaceholders;
public ConfigAwarePlaceholdersTests()
{
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddJsonFile("appsettings.json");
var configuration = configurationBuilder.Build();
_basePlaceholders = new Mock<IPlaceholders>();
_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<string>(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<string>(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<string>(new FakeError()));
var result = _placeholders.Get(key);
result.Data.ShouldBe(expected);
}
[Fact]
public void should_call_underyling_when_added()
{
const string key = "{Test}";
Func<Response<string>> func = () => new OkResponse<string>("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);
}
}
}

View File

@ -20,5 +20,10 @@
"port": 5000, "port": 5000,
"hostName": "localhost" "hostName": "localhost"
} }
},
"BaseUrl": "http://foo-bar.co.uk",
"TestConfig": "foo",
"TestConfigNested":{
"Child": "foo"
} }
} }