started refactoring request builder

This commit is contained in:
TomPallister 2016-10-30 18:35:05 +00:00
parent 56bf4014bd
commit 5fc04a0514
13 changed files with 248 additions and 134 deletions

View File

@ -78,6 +78,14 @@ This is pretty much all you need to get going.......more to come!
## Logging ## Logging
Ocelot uses the standard logging interfaces ILoggerFactory / ILogger<T> as such you can use any logging provider you like such as default, nlog, serilog. Ocelot uses the standard logging interfaces ILoggerFactory / ILogger<T> as such you can use any logging provider you like such as default, nlog, serilog or whatever you want.
## Not supported
Ocelot does not support...
- Chunked Encoding - Ocelot will always get the body size and return Content-Length header. Sorry
if this doesn't work for your use case!
- Fowarding a host header - The host header that you send to Ocelot will not be forwarded to
the downstream service. Obviously this would break everything :(

View File

@ -46,7 +46,7 @@ namespace Ocelot.DependencyInjection
services.AddLogging(); services.AddLogging();
// ocelot services. // ocelot services.
services.AddSingleton<IRemoveHeaders, RemoveHeaders>(); services.AddSingleton<IRemoveOutputHeaders, RemoveOutputHeaders>();
services.AddSingleton<IOcelotConfigurationProvider, OcelotConfigurationProvider>(); services.AddSingleton<IOcelotConfigurationProvider, OcelotConfigurationProvider>();
services.AddSingleton<IClaimToThingConfigurationParser, ClaimToThingConfigurationParser>(); services.AddSingleton<IClaimToThingConfigurationParser, ClaimToThingConfigurationParser>();
services.AddSingleton<IAuthoriser, ClaimsAuthoriser>(); services.AddSingleton<IAuthoriser, ClaimsAuthoriser>();
@ -60,7 +60,7 @@ namespace Ocelot.DependencyInjection
services.AddSingleton<IDownstreamRouteFinder, DownstreamRouteFinder.Finder.DownstreamRouteFinder>(); services.AddSingleton<IDownstreamRouteFinder, DownstreamRouteFinder.Finder.DownstreamRouteFinder>();
services.AddSingleton<IHttpRequester, HttpClientHttpRequester>(); services.AddSingleton<IHttpRequester, HttpClientHttpRequester>();
services.AddSingleton<IHttpResponder, HttpContextResponder>(); services.AddSingleton<IHttpResponder, HttpContextResponder>();
services.AddSingleton<IRequestBuilder, HttpRequestBuilder>(); services.AddSingleton<IRequestCreator, HttpRequestCreator>();
services.AddSingleton<IErrorsToHttpStatusCodeMapper, ErrorsToHttpStatusCodeMapper>(); services.AddSingleton<IErrorsToHttpStatusCodeMapper, ErrorsToHttpStatusCodeMapper>();
services.AddSingleton<IAuthenticationHandlerFactory, AuthenticationHandlerFactory>(); services.AddSingleton<IAuthenticationHandlerFactory, AuthenticationHandlerFactory>();
services.AddSingleton<IAuthenticationHandlerCreator, AuthenticationHandlerCreator>(); services.AddSingleton<IAuthenticationHandlerCreator, AuthenticationHandlerCreator>();

View File

@ -3,7 +3,7 @@ using Ocelot.Responses;
namespace Ocelot.Headers namespace Ocelot.Headers
{ {
public interface IRemoveHeaders public interface IRemoveOutputHeaders
{ {
Response Remove(HttpResponseHeaders headers); Response Remove(HttpResponseHeaders headers);
} }

View File

@ -1,22 +1,20 @@
using System.Collections.Generic;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using Ocelot.Responses; using Ocelot.Responses;
namespace Ocelot.Headers namespace Ocelot.Headers
{ {
public class RemoveHeaders : IRemoveHeaders public class RemoveOutputHeaders : IRemoveOutputHeaders
{ {
/// <summary> /// <summary>
/// Some webservers return headers that cannot be forwarded to the client /// Some webservers return headers that cannot be forwarded to the client
/// in a given context such as transfer encoding chunked when ASP.NET is not /// in a given context such as transfer encoding chunked when ASP.NET is not
/// returning the response in this manner /// returning the response in this manner
/// </summary> /// </summary>
private readonly List<string> _unsupportedHeaders = new List<string> private readonly string[] _unsupportedHeaders =
{ {
"Transfer-Encoding" "Transfer-Encoding"
}; };
public Response Remove(HttpResponseHeaders headers) public Response Remove(HttpResponseHeaders headers)
{ {
foreach (var unsupported in _unsupportedHeaders) foreach (var unsupported in _unsupportedHeaders)

View File

@ -1,109 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Ocelot.Responses;
namespace Ocelot.Request.Builder
{
public class HttpRequestBuilder : IRequestBuilder
{
public async Task<Response<Request>> Build(
string httpMethod,
string downstreamUrl,
Stream content,
IHeaderDictionary headers,
IRequestCookieCollection cookies,
QueryString queryString,
string contentType,
RequestId.RequestId requestId)
{
var method = new HttpMethod(httpMethod);
var uri = new Uri(string.Format("{0}{1}", downstreamUrl, queryString.ToUriComponent()));
var httpRequestMessage = new HttpRequestMessage(method, uri);
if (content != null)
{
httpRequestMessage.Content = new ByteArrayContent(await ToByteArray(content));
}
if (!string.IsNullOrEmpty(contentType))
{
httpRequestMessage.Content.Headers.Remove("Content-Type");
httpRequestMessage.Content.Headers.TryAddWithoutValidation("Content-Type", contentType);
}
//todo get rid of if
if (headers != null)
{
headers.Remove("Content-Type");
foreach (var header in headers)
{
//todo get rid of if..
if (header.Key.ToLower() != "host")
{
httpRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
}
}
}
if (RequestKeyIsNotNull(requestId) && !RequestIdInHeaders(requestId, httpRequestMessage.Headers))
{
ForwardRequestIdToDownstreamService(requestId, httpRequestMessage);
}
var cookieContainer = new CookieContainer();
//todo get rid of if
if (cookies != null)
{
foreach (var cookie in cookies)
{
cookieContainer.Add(uri, new Cookie(cookie.Key, cookie.Value));
}
}
return new OkResponse<Request>(new Request(httpRequestMessage, cookieContainer));
}
private void ForwardRequestIdToDownstreamService(RequestId.RequestId requestId, HttpRequestMessage httpRequestMessage)
{
httpRequestMessage.Headers.Add(requestId.RequestIdKey, requestId.RequestIdValue);
}
private bool RequestIdInHeaders(RequestId.RequestId requestId, HttpRequestHeaders headers)
{
IEnumerable<string> value;
if (headers.TryGetValues(requestId.RequestIdKey, out value))
{
return true;
}
return false;
}
private bool RequestKeyIsNotNull(RequestId.RequestId requestId)
{
return !string.IsNullOrEmpty(requestId?.RequestIdKey) && !string.IsNullOrEmpty(requestId.RequestIdValue);
}
private async Task<byte[]> ToByteArray(Stream stream)
{
using (stream)
{
using (var memStream = new MemoryStream())
{
await stream.CopyToAsync(memStream);
return memStream.ToArray();
}
}
}
}
}

View File

@ -0,0 +1,34 @@
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Ocelot.Responses;
namespace Ocelot.Request.Builder
{
public sealed class HttpRequestCreator : IRequestCreator
{
public async Task<Response<Request>> Build(
string httpMethod,
string downstreamUrl,
Stream content,
IHeaderDictionary headers,
IRequestCookieCollection cookies,
QueryString queryString,
string contentType,
RequestId.RequestId requestId)
{
var request = await new RequestBuilder()
.WithHttpMethod(httpMethod)
.WithDownstreamUrl(downstreamUrl)
.WithQueryString(queryString)
.WithContent(content)
.WithContentType(contentType)
.WithHeaders(headers)
.WithRequestId(requestId)
.WithCookies(cookies)
.Build();
return new OkResponse<Request>(request);
}
}
}

View File

@ -5,7 +5,7 @@ using Ocelot.Responses;
namespace Ocelot.Request.Builder namespace Ocelot.Request.Builder
{ {
public interface IRequestBuilder public interface IRequestCreator
{ {
Task<Response<Request>> Build(string httpMethod, Task<Response<Request>> Build(string httpMethod,
string downstreamUrl, string downstreamUrl,

View File

@ -0,0 +1,183 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
namespace Ocelot.Request.Builder
{
internal sealed class RequestBuilder
{
private HttpMethod _method;
private string _downstreamUrl;
private QueryString _queryString;
private Stream _content;
private string _contentType;
private IHeaderDictionary _headers;
private RequestId.RequestId _requestId;
private IRequestCookieCollection _cookies;
private readonly string[] _unsupportedHeaders = {"host"};
public RequestBuilder WithHttpMethod(string httpMethod)
{
_method = new HttpMethod(httpMethod);
return this;
}
public RequestBuilder WithDownstreamUrl(string downstreamUrl)
{
_downstreamUrl = downstreamUrl;
return this;
}
public RequestBuilder WithQueryString(QueryString queryString)
{
_queryString = queryString;
return this;
}
public RequestBuilder WithContent(Stream content)
{
_content = content;
return this;
}
public RequestBuilder WithContentType(string contentType)
{
_contentType = contentType;
return this;
}
public RequestBuilder WithHeaders(IHeaderDictionary headers)
{
_headers = headers;
return this;
}
public RequestBuilder WithRequestId(RequestId.RequestId requestId)
{
_requestId = requestId;
return this;
}
public RequestBuilder WithCookies(IRequestCookieCollection cookies)
{
_cookies = cookies;
return this;
}
public async Task<Request> Build()
{
var uri = new Uri(string.Format("{0}{1}", _downstreamUrl, _queryString.ToUriComponent()));
var httpRequestMessage = new HttpRequestMessage(_method, uri);
await AddContentToRequest(httpRequestMessage);
AddContentTypeToRequest(httpRequestMessage);
AddHeadersToRequest(httpRequestMessage);
if (RequestIdKeyIsNotNull(_requestId) && !RequestIdInHeaders(_requestId, httpRequestMessage.Headers))
{
AddRequestIdHeader(_requestId, httpRequestMessage);
}
var cookieContainer = CreateCookieContainer(uri);
return new Request(httpRequestMessage, cookieContainer);
}
private async Task AddContentToRequest(HttpRequestMessage httpRequestMessage)
{
if (_content != null)
{
httpRequestMessage.Content = new ByteArrayContent(await ToByteArray(_content));
}
}
private void AddContentTypeToRequest(HttpRequestMessage httpRequestMessage)
{
if (!string.IsNullOrEmpty(_contentType))
{
httpRequestMessage.Content.Headers.Remove("Content-Type");
httpRequestMessage.Content.Headers.TryAddWithoutValidation("Content-Type", _contentType);
}
}
private void AddHeadersToRequest(HttpRequestMessage httpRequestMessage)
{
if (_headers != null)
{
_headers.Remove("Content-Type");
foreach (var header in _headers)
{
//todo get rid of if..
if (IsSupportedHeader(header))
{
httpRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
}
}
}
}
private bool IsSupportedHeader(KeyValuePair<string, StringValues> header)
{
return !_unsupportedHeaders.Contains(header.Key.ToLower());
}
private CookieContainer CreateCookieContainer(Uri uri)
{
var cookieContainer = new CookieContainer();
if (_cookies != null)
{
foreach (var cookie in _cookies)
{
cookieContainer.Add(uri, new Cookie(cookie.Key, cookie.Value));
}
}
return cookieContainer;
}
private void AddRequestIdHeader(RequestId.RequestId requestId, HttpRequestMessage httpRequestMessage)
{
httpRequestMessage.Headers.Add(requestId.RequestIdKey, requestId.RequestIdValue);
}
private bool RequestIdInHeaders(RequestId.RequestId requestId, HttpRequestHeaders headers)
{
IEnumerable<string> value;
if (headers.TryGetValues(requestId.RequestIdKey, out value))
{
return true;
}
return false;
}
private bool RequestIdKeyIsNotNull(RequestId.RequestId requestId)
{
return !string.IsNullOrEmpty(requestId?.RequestIdKey) && !string.IsNullOrEmpty(requestId.RequestIdValue);
}
private async Task<byte[]> ToByteArray(Stream stream)
{
using (stream)
{
using (var memStream = new MemoryStream())
{
await stream.CopyToAsync(memStream);
return memStream.ToArray();
}
}
}
}
}

View File

@ -9,20 +9,20 @@ namespace Ocelot.Request.Middleware
public class HttpRequestBuilderMiddleware : OcelotMiddleware public class HttpRequestBuilderMiddleware : OcelotMiddleware
{ {
private readonly RequestDelegate _next; private readonly RequestDelegate _next;
private readonly IRequestBuilder _requestBuilder; private readonly IRequestCreator _requestCreator;
public HttpRequestBuilderMiddleware(RequestDelegate next, public HttpRequestBuilderMiddleware(RequestDelegate next,
IRequestScopedDataRepository requestScopedDataRepository, IRequestScopedDataRepository requestScopedDataRepository,
IRequestBuilder requestBuilder) IRequestCreator requestCreator)
:base(requestScopedDataRepository) :base(requestScopedDataRepository)
{ {
_next = next; _next = next;
_requestBuilder = requestBuilder; _requestCreator = requestCreator;
} }
public async Task Invoke(HttpContext context) public async Task Invoke(HttpContext context)
{ {
var buildResult = await _requestBuilder var buildResult = await _requestCreator
.Build(context.Request.Method, DownstreamUrl, context.Request.Body, .Build(context.Request.Method, DownstreamUrl, context.Request.Body,
context.Request.Headers, context.Request.Cookies, context.Request.QueryString, context.Request.Headers, context.Request.Cookies, context.Request.QueryString,
context.Request.ContentType, new RequestId.RequestId(DownstreamRoute?.ReRoute?.RequestIdKey, context.TraceIdentifier)); context.Request.ContentType, new RequestId.RequestId(DownstreamRoute?.ReRoute?.RequestIdKey, context.TraceIdentifier));

View File

@ -15,16 +15,16 @@ namespace Ocelot.Responder
/// </summary> /// </summary>
public class HttpContextResponder : IHttpResponder public class HttpContextResponder : IHttpResponder
{ {
private readonly IRemoveHeaders _removeHeaders; private readonly IRemoveOutputHeaders _removeOutputHeaders;
public HttpContextResponder(IRemoveHeaders removeHeaders) public HttpContextResponder(IRemoveOutputHeaders removeOutputHeaders)
{ {
_removeHeaders = removeHeaders; _removeOutputHeaders = removeOutputHeaders;
} }
public async Task<Response> SetResponseOnHttpContext(HttpContext context, HttpResponseMessage response) public async Task<Response> SetResponseOnHttpContext(HttpContext context, HttpResponseMessage response)
{ {
_removeHeaders.Remove(response.Headers); _removeOutputHeaders.Remove(response.Headers);
foreach (var httpResponseHeader in response.Headers) foreach (var httpResponseHeader in response.Headers)
{ {

View File

@ -10,12 +10,12 @@ namespace Ocelot.UnitTests.Headers
public class RemoveHeaders public class RemoveHeaders
{ {
private HttpResponseHeaders _headers; private HttpResponseHeaders _headers;
private readonly Ocelot.Headers.RemoveHeaders _removeHeaders; private readonly Ocelot.Headers.RemoveOutputHeaders _removeOutputHeaders;
private Response _result; private Response _result;
public RemoveHeaders() public RemoveHeaders()
{ {
_removeHeaders = new Ocelot.Headers.RemoveHeaders(); _removeOutputHeaders = new Ocelot.Headers.RemoveOutputHeaders();
} }
[Fact] [Fact]
@ -39,7 +39,7 @@ namespace Ocelot.UnitTests.Headers
private void WhenIRemoveTheHeaders() private void WhenIRemoveTheHeaders()
{ {
_result = _removeHeaders.Remove(_headers); _result = _removeOutputHeaders.Remove(_headers);
} }
private void TheHeaderIsNoLongerInTheContext() private void TheHeaderIsNoLongerInTheContext()

View File

@ -22,7 +22,7 @@ namespace Ocelot.UnitTests.Request
{ {
public class HttpRequestBuilderMiddlewareTests : IDisposable public class HttpRequestBuilderMiddlewareTests : IDisposable
{ {
private readonly Mock<IRequestBuilder> _requestBuilder; private readonly Mock<IRequestCreator> _requestBuilder;
private readonly Mock<IRequestScopedDataRepository> _scopedRepository; private readonly Mock<IRequestScopedDataRepository> _scopedRepository;
private readonly string _url; private readonly string _url;
private readonly TestServer _server; private readonly TestServer _server;
@ -35,7 +35,7 @@ namespace Ocelot.UnitTests.Request
public HttpRequestBuilderMiddlewareTests() public HttpRequestBuilderMiddlewareTests()
{ {
_url = "http://localhost:51879"; _url = "http://localhost:51879";
_requestBuilder = new Mock<IRequestBuilder>(); _requestBuilder = new Mock<IRequestCreator>();
_scopedRepository = new Mock<IRequestScopedDataRepository>(); _scopedRepository = new Mock<IRequestScopedDataRepository>();
var builder = new WebHostBuilder() var builder = new WebHostBuilder()

View File

@ -22,14 +22,14 @@ namespace Ocelot.UnitTests.Request
private IRequestCookieCollection _cookies; private IRequestCookieCollection _cookies;
private QueryString _query; private QueryString _query;
private string _contentType; private string _contentType;
private readonly IRequestBuilder _requestBuilder; private readonly IRequestCreator _requestCreator;
private Response<Ocelot.Request.Request> _result; private Response<Ocelot.Request.Request> _result;
private Ocelot.RequestId.RequestId _requestId; private Ocelot.RequestId.RequestId _requestId;
public RequestBuilderTests() public RequestBuilderTests()
{ {
_content = new StringContent(string.Empty); _content = new StringContent(string.Empty);
_requestBuilder = new HttpRequestBuilder(); _requestCreator = new HttpRequestCreator();
} }
[Fact] [Fact]
@ -280,7 +280,7 @@ namespace Ocelot.UnitTests.Request
private void WhenICreateARequest() private void WhenICreateARequest()
{ {
_result = _requestBuilder.Build(_httpMethod, _downstreamUrl, _content?.ReadAsStreamAsync().Result, _headers, _result = _requestCreator.Build(_httpMethod, _downstreamUrl, _content?.ReadAsStreamAsync().Result, _headers,
_cookies, _query, _contentType, _requestId).Result; _cookies, _query, _contentType, _requestId).Result;
} }