diff --git a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs index c076f367..04ca1037 100644 --- a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs @@ -135,6 +135,7 @@ namespace Ocelot.DependencyInjection services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc // could maybe use a scoped data repository diff --git a/src/Ocelot/Requester/HttpClientBuilder.cs b/src/Ocelot/Requester/HttpClientBuilder.cs index da37ee02..8cad9d49 100644 --- a/src/Ocelot/Requester/HttpClientBuilder.cs +++ b/src/Ocelot/Requester/HttpClientBuilder.cs @@ -1,46 +1,110 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Net.Http; +using System.Threading.Tasks; using Ocelot.Logging; using Ocelot.Requester.QoS; namespace Ocelot.Requester { - internal class HttpClientBuilder + internal class HttpClientBuilder : IHttpClientBuilder { - private readonly Dictionary> _handlers = new Dictionary>(); + private TimeSpan? _timeout; + private readonly List _handlers = new List(); + private Dictionary _defaultHeaders; + private CookieContainer _cookieContainer; + private IQoSProvider _qoSProvider; - public HttpClientBuilder WithQoS(IQoSProvider qoSProvider, IOcelotLogger logger, HttpMessageHandler innerHandler) + public IHttpClientBuilder WithCookieContainer(CookieContainer cookieContainer) { - _handlers.Add(5000, () => new PollyCircuitBreakingDelegatingHandler(qoSProvider, logger, innerHandler)); + _cookieContainer = cookieContainer; return this; } - internal HttpClient Build(HttpMessageHandler innerHandler) + public IHttpClientBuilder WithTimeout(TimeSpan timeout) { - return _handlers.Any() ? - new HttpClient(CreateHttpMessageHandler()) : - new HttpClient(innerHandler); + _timeout = timeout; + return this; } - private HttpMessageHandler CreateHttpMessageHandler() + public IHttpClientBuilder WithHandler(DelegatingHandler handler) { - HttpMessageHandler httpMessageHandler = new HttpClientHandler(); + _handlers.Add(handler); + return this; + } - _handlers - .OrderByDescending(handler => handler.Key) - .Select(handler => handler.Value) - .Reverse() - .ToList() - .ForEach(handler => + public IHttpClientBuilder WithDefaultRequestHeaders(Dictionary headers) + { + _defaultHeaders = headers; + return this; + } + + + public IHttpClient Create() + { + HttpClientHandler httpclientHandler = null; + if (_cookieContainer != null) { - var delegatingHandler = handler(); - delegatingHandler.InnerHandler = httpMessageHandler; - httpMessageHandler = delegatingHandler; - }); + httpclientHandler = new HttpClientHandler() { CookieContainer = _cookieContainer }; + } + else + { + httpclientHandler = new HttpClientHandler(); + } + if (httpclientHandler.SupportsAutomaticDecompression) + { + httpclientHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + } + + var client = new HttpClient(CreateHttpMessageHandler(httpclientHandler)); + + if (_timeout.HasValue) + { + client.Timeout = _timeout.Value; + } + + if (_defaultHeaders == null) + { + return new HttpClientWrapper(client); + } + + foreach (var header in _defaultHeaders) + { + client.DefaultRequestHeaders.Add(header.Key, header.Value); + } + + return new HttpClientWrapper(client); + } + + private HttpMessageHandler CreateHttpMessageHandler(HttpMessageHandler httpMessageHandler) + { + foreach (var handler in _handlers) + { + handler.InnerHandler = httpMessageHandler; + httpMessageHandler = handler; + } return httpMessageHandler; } } + + /// + /// This class was made to make unit testing easier when HttpClient is used. + /// + internal class HttpClientWrapper : IHttpClient + { + public HttpClient Client { get; } + + public HttpClientWrapper(HttpClient client) + { + Client = client; + } + + public Task SendAsync(HttpRequestMessage request) + { + return Client.SendAsync(request); + } + } } diff --git a/src/Ocelot/Requester/HttpClientHttpRequester.cs b/src/Ocelot/Requester/HttpClientHttpRequester.cs index 5a2c86c7..f0566fed 100644 --- a/src/Ocelot/Requester/HttpClientHttpRequester.cs +++ b/src/Ocelot/Requester/HttpClientHttpRequester.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Net.Http; using System.Threading.Tasks; using Ocelot.Logging; @@ -10,47 +11,56 @@ namespace Ocelot.Requester { public class HttpClientHttpRequester : IHttpRequester { + private IHttpClientMessageCacheHandler _cacheHandlers; private readonly IOcelotLogger _logger; - public HttpClientHttpRequester(IOcelotLoggerFactory loggerFactory) + public HttpClientHttpRequester(IOcelotLoggerFactory loggerFactory, IHttpClientMessageCacheHandler cacheHandlers) { _logger = loggerFactory.CreateLogger(); + _cacheHandlers = cacheHandlers; } public async Task> GetResponse(Request.Request request) { - var builder = new HttpClientBuilder(); + var builder = new HttpClientBuilder(); - using (var handler = new HttpClientHandler { CookieContainer = request.CookieContainer }) + builder.WithCookieContainer(request.CookieContainer); + + string baseUrl = $"{request.HttpRequestMessage.RequestUri.Scheme}://{request.HttpRequestMessage.RequestUri.Authority}"; + + if (request.IsQos) { - if (request.IsQos) - { - builder.WithQoS(request.QosProvider, _logger, handler); - } - - using (var httpClient = builder.Build(handler)) - { - try - { - var response = await httpClient.SendAsync(request.HttpRequestMessage); - return new OkResponse(response); - } - catch (TimeoutRejectedException exception) - { - return - new ErrorResponse(new RequestTimedOutError(exception)); - } - catch (BrokenCircuitException exception) - { - return - new ErrorResponse(new RequestTimedOutError(exception)); - } - catch (Exception exception) - { - return new ErrorResponse(new UnableToCompleteRequestError(exception)); - } - } + builder.WithHandler(new PollyCircuitBreakingDelegatingHandler(request.QosProvider, _logger)); + baseUrl = $"{baseUrl}{request.QosProvider.CircuitBreaker.CircuitBreakerPolicy.PolicyKey}"; } + + IHttpClient httpClient = _cacheHandlers.Get(baseUrl); + if (httpClient == null) + { + httpClient = builder.Create(); + _cacheHandlers.Set(baseUrl, httpClient, TimeSpan.FromMinutes(30)); + } + + try + { + var response = await httpClient.SendAsync(request.HttpRequestMessage); + return new OkResponse(response); + } + catch (TimeoutRejectedException exception) + { + return + new ErrorResponse(new RequestTimedOutError(exception)); + } + catch (BrokenCircuitException exception) + { + return + new ErrorResponse(new RequestTimedOutError(exception)); + } + catch (Exception exception) + { + return new ErrorResponse(new UnableToCompleteRequestError(exception)); + } + } } } \ No newline at end of file diff --git a/src/Ocelot/Requester/IHttpClient.cs b/src/Ocelot/Requester/IHttpClient.cs new file mode 100644 index 00000000..8a169a36 --- /dev/null +++ b/src/Ocelot/Requester/IHttpClient.cs @@ -0,0 +1,13 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Ocelot.Requester +{ + public interface IHttpClient + { + HttpClient Client { get; } + + Task SendAsync(HttpRequestMessage request); + } +} diff --git a/src/Ocelot/Requester/IHttpClientBuilder.cs b/src/Ocelot/Requester/IHttpClientBuilder.cs new file mode 100644 index 00000000..85fd5f69 --- /dev/null +++ b/src/Ocelot/Requester/IHttpClientBuilder.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Ocelot.Logging; +using Ocelot.Requester.QoS; +using System.Net; + +namespace Ocelot.Requester +{ + public interface IHttpClientBuilder + { + /// + /// Sets the cookie container used to store server cookies by the handler + /// + /// + /// + IHttpClientBuilder WithCookieContainer(CookieContainer cookieContainer); + + /// + /// Sets the number of milliseconds to wait before the request times out. + /// + IHttpClientBuilder WithTimeout(TimeSpan timeout); + + /// + /// Sets a DelegatingHandler. + /// Can be reused to set several different Handlers in a pipeline. + /// + IHttpClientBuilder WithHandler(DelegatingHandler handler); + + /// + /// Sets Default HttpRequestHeaders + /// + IHttpClientBuilder WithDefaultRequestHeaders(Dictionary headers); + + /// + /// Creates the + /// + IHttpClient Create(); + } +} diff --git a/src/Ocelot/Requester/IHttpClientMessageCacheHandler.cs b/src/Ocelot/Requester/IHttpClientMessageCacheHandler.cs new file mode 100644 index 00000000..a2a93692 --- /dev/null +++ b/src/Ocelot/Requester/IHttpClientMessageCacheHandler.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Ocelot.Requester +{ + public interface IHttpClientMessageCacheHandler + { + bool Exists(string id); + IHttpClient Get(string id); + void Remove(string id); + void Set(string id, IHttpClient handler, TimeSpan expirationTime); + } +} diff --git a/src/Ocelot/Requester/MemoryHttpClientMessageCacheHandler.cs b/src/Ocelot/Requester/MemoryHttpClientMessageCacheHandler.cs new file mode 100644 index 00000000..bb948b8a --- /dev/null +++ b/src/Ocelot/Requester/MemoryHttpClientMessageCacheHandler.cs @@ -0,0 +1,46 @@ +using Microsoft.Extensions.Caching.Memory; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Ocelot.Requester +{ + public class MemoryHttpClientMessageCacheHandler : IHttpClientMessageCacheHandler + { + private readonly IMemoryCache _memoryCache; + + public MemoryHttpClientMessageCacheHandler(IMemoryCache memoryCache) + { + _memoryCache = memoryCache; + } + + public void Set(string id, IHttpClient counter, TimeSpan expirationTime) + { + _memoryCache.Set(id, counter, new MemoryCacheEntryOptions().SetAbsoluteExpiration(expirationTime)); + } + + public bool Exists(string id) + { + IHttpClient counter; + return _memoryCache.TryGetValue(id, out counter); + } + + public IHttpClient Get(string id) + { + IHttpClient counter; + if (_memoryCache.TryGetValue(id, out counter)) + { + return counter; + } + + return null; + } + + public void Remove(string id) + { + _memoryCache.Remove(id); + } + } +} diff --git a/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs b/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs index a50ec91b..a09ed233 100644 --- a/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs +++ b/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs @@ -16,9 +16,7 @@ namespace Ocelot.Requester public PollyCircuitBreakingDelegatingHandler( IQoSProvider qoSProvider, - IOcelotLogger logger, - HttpMessageHandler innerHandler) - : base(innerHandler) + IOcelotLogger logger) { _qoSProvider = qoSProvider; _logger = logger;