Merge branch 'pitming-feature/MethodTransformer'

This commit is contained in:
TomPallister 2020-02-09 16:55:58 +00:00
commit 689f04d011
17 changed files with 293 additions and 27 deletions

View File

@ -24,6 +24,7 @@ Here is an example ReRoute configuration, You don't need to set all of these thi
"UpstreamHttpMethod": [
"Get"
],
"DownstreamHttpMethod": "",
"AddHeadersToRequest": {},
"AddClaimsToRequest": {},
"RouteClaimsRequirement": {},

View File

@ -0,0 +1,28 @@
HTTP Method Transformation
==========================
Ocelot allows the user to change the HTTP request method that will be used when making a request to a downstream service.
This achieved by setting the following ReRoute configuration:
.. code-block:: json
{
"DownstreamPathTemplate": "/{url}",
"UpstreamPathTemplate": "/{url}",
"UpstreamHttpMethod": [
"Get"
],
"DownstreamHttpMethod": "POST",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 53271
}
],
}
The key property here is DownstreamHttpMethod which is set as POST and the ReRoute will only match on GET as set by UpstreamHttpMethod.
This feature can be useful when interacting with downstream apis that only support POST and you want to present some kind of RESTful interface.

View File

@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Ocelot.Provider.Kubernetes;
@ -18,7 +19,7 @@ namespace ApiGateway
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{

View File

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -28,7 +29,7 @@ namespace DownstreamService
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{

View File

@ -41,6 +41,7 @@ namespace Ocelot.Configuration.Builder
private List<AddHeader> _addHeadersToUpstream;
private bool _dangerousAcceptAnyServerCertificateValidator;
private SecurityOptions _securityOptions;
private string _downstreamHttpMethod;
public DownstreamReRouteBuilder()
{
@ -56,6 +57,12 @@ namespace Ocelot.Configuration.Builder
return this;
}
public DownstreamReRouteBuilder WithDownStreamHttpMethod(string method)
{
_downstreamHttpMethod = method;
return this;
}
public DownstreamReRouteBuilder WithLoadBalancerOptions(LoadBalancerOptions loadBalancerOptions)
{
_loadBalancerOptions = loadBalancerOptions;
@ -282,7 +289,8 @@ namespace Ocelot.Configuration.Builder
_addHeadersToDownstream,
_addHeadersToUpstream,
_dangerousAcceptAnyServerCertificateValidator,
_securityOptions);
_securityOptions,
_downstreamHttpMethod);
}
}
}

View File

@ -138,6 +138,7 @@ namespace Ocelot.Configuration.Creator
.WithAddHeadersToUpstream(hAndRs.AddHeadersToUpstream)
.WithDangerousAcceptAnyServerCertificateValidator(fileReRoute.DangerousAcceptAnyServerCertificateValidator)
.WithSecurityOptions(securityOptions)
.WithDownStreamHttpMethod(fileReRoute.DownstreamHttpMethod)
.Build();
return reRoute;

View File

@ -38,7 +38,8 @@ namespace Ocelot.Configuration
List<AddHeader> addHeadersToDownstream,
List<AddHeader> addHeadersToUpstream,
bool dangerousAcceptAnyServerCertificateValidator,
SecurityOptions securityOptions)
SecurityOptions securityOptions,
string downstreamHttpMethod)
{
DangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator;
AddHeadersToDownstream = addHeadersToDownstream;
@ -72,6 +73,7 @@ namespace Ocelot.Configuration
LoadBalancerKey = loadBalancerKey;
AddHeadersToUpstream = addHeadersToUpstream;
SecurityOptions = securityOptions;
DownstreamHttpMethod = downstreamHttpMethod;
}
public string Key { get; }
@ -106,5 +108,6 @@ namespace Ocelot.Configuration
public List<AddHeader> AddHeadersToUpstream { get; }
public bool DangerousAcceptAnyServerCertificateValidator { get; }
public SecurityOptions SecurityOptions { get; }
public string DownstreamHttpMethod { get; }
}
}

View File

@ -29,6 +29,7 @@ namespace Ocelot.Configuration.File
public string DownstreamPathTemplate { get; set; }
public string UpstreamPathTemplate { get; set; }
public List<string> UpstreamHttpMethod { get; set; }
public string DownstreamHttpMethod { get; set; }
public Dictionary<string, string> AddHeadersToRequest { get; set; }
public Dictionary<string, string> UpstreamHeaderTransform { get; set; }
public Dictionary<string, string> DownstreamHeaderTransform { get; set; }

View File

@ -1,12 +1,13 @@
namespace Ocelot.Request.Mapper
{
using Microsoft.AspNetCore.Http;
using Ocelot.Configuration;
using Ocelot.Responses;
using System.Net.Http;
using System.Threading.Tasks;
public interface IRequestMapper
{
Task<Response<HttpRequestMessage>> Map(HttpRequest request);
Task<Response<HttpRequestMessage>> Map(HttpRequest request, DownstreamReRoute downstreamReRoute);
}
}

View File

@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Primitives;
using Ocelot.Configuration;
using Ocelot.Responses;
using System;
using System.Collections.Generic;
@ -15,14 +16,14 @@
{
private readonly string[] _unsupportedHeaders = { "host" };
public async Task<Response<HttpRequestMessage>> Map(HttpRequest request)
public async Task<Response<HttpRequestMessage>> Map(HttpRequest request, DownstreamReRoute downstreamReRoute)
{
try
{
var requestMessage = new HttpRequestMessage()
{
Content = await MapContent(request),
Method = MapMethod(request),
Method = MapMethod(request, downstreamReRoute),
RequestUri = MapUri(request)
};
@ -71,8 +72,13 @@
}
}
private HttpMethod MapMethod(HttpRequest request)
private HttpMethod MapMethod(HttpRequest request, DownstreamReRoute downstreamReRoute)
{
if (!string.IsNullOrEmpty(downstreamReRoute?.DownstreamHttpMethod))
{
return new HttpMethod(downstreamReRoute.DownstreamHttpMethod);
}
return new HttpMethod(request.Method);
}

View File

@ -52,6 +52,7 @@ namespace Ocelot.Request.Middleware
};
_request.RequestUri = uriBuilder.Uri;
_request.Method = new HttpMethod(Method);
return _request;
}

View File

@ -24,7 +24,7 @@ namespace Ocelot.Request.Middleware
public async Task Invoke(DownstreamContext context)
{
var downstreamRequest = await _requestMapper.Map(context.HttpContext.Request);
var downstreamRequest = await _requestMapper.Map(context.HttpContext.Request, context.DownstreamReRoute);
if (downstreamRequest.IsError)
{

View File

@ -1,4 +1,3 @@
using Microsoft.AspNetCore.Builder;
using Ocelot.Middleware.Pipeline;
namespace Ocelot.Request.Middleware

View File

@ -0,0 +1,158 @@
namespace Ocelot.AcceptanceTests
{
using Microsoft.AspNetCore.Http;
using Ocelot.Configuration.File;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using TestStack.BDDfy;
using Xunit;
public class MethodTests : IDisposable
{
private readonly Steps _steps;
private readonly ServiceHandler _serviceHandler;
public MethodTests()
{
_serviceHandler = new ServiceHandler();
_steps = new Steps();
}
[Fact]
public void should_return_response_200_when_get_converted_to_post()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/{url}",
DownstreamScheme = "http",
UpstreamPathTemplate = "/{url}",
UpstreamHttpMethod = new List<string> { "Get" },
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 53171,
},
},
DownstreamHttpMethod = "POST",
},
},
};
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:53171/", "/", "POST"))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.BDDfy();
}
[Fact]
public void should_return_response_200_when_get_converted_to_post_with_content()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/{url}",
DownstreamScheme = "http",
UpstreamPathTemplate = "/{url}",
UpstreamHttpMethod = new List<string> { "Get" },
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 53271,
},
},
DownstreamHttpMethod = "POST",
},
},
};
const string expected = "here is some content";
var httpContent = new StringContent(expected);
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:53271/", "/", "POST"))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/", httpContent))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(_ => _steps.ThenTheResponseBodyShouldBe(expected))
.BDDfy();
}
[Fact]
public void should_return_response_200_when_get_converted_to_get_with_content()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/{url}",
DownstreamScheme = "http",
UpstreamPathTemplate = "/{url}",
UpstreamHttpMethod = new List<string> { "Post" },
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 53272,
},
},
DownstreamHttpMethod = "GET",
},
},
};
const string expected = "here is some content";
var httpContent = new StringContent(expected);
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:53272/", "/", "GET"))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIPostUrlOnTheApiGateway("/", httpContent))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(_ => _steps.ThenTheResponseBodyShouldBe(expected))
.BDDfy();
}
private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, string expected)
{
_serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context =>
{
if (context.Request.Method == expected)
{
context.Response.StatusCode = 200;
var reader = new StreamReader(context.Request.Body);
var body = await reader.ReadToEndAsync();
await context.Response.WriteAsync(body);
}
else
{
context.Response.StatusCode = 500;
}
});
}
public void Dispose()
{
_serviceHandler.Dispose();
_steps.Dispose();
}
}
}

View File

@ -909,6 +909,18 @@ namespace Ocelot.AcceptanceTests
_response = _ocelotClient.GetAsync(url).Result;
}
public void WhenIGetUrlOnTheApiGateway(string url, HttpContent content)
{
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url) {Content = content};
_response = _ocelotClient.SendAsync(httpRequestMessage).Result;
}
public void WhenIPostUrlOnTheApiGateway(string url, HttpContent content)
{
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, url) { Content = content };
_response = _ocelotClient.SendAsync(httpRequestMessage).Result;
}
public void WhenIGetUrlOnTheApiGateway(string url, string cookie, string value)
{
var request = _ocelotServer.CreateRequest(url);

View File

@ -1,6 +1,4 @@
using Ocelot.Middleware;
namespace Ocelot.UnitTests.Request
namespace Ocelot.UnitTests.Request
{
using Microsoft.AspNetCore.Http;
using Moq;
@ -9,9 +7,12 @@ namespace Ocelot.UnitTests.Request
using Ocelot.Request.Creator;
using Ocelot.Request.Mapper;
using Ocelot.Request.Middleware;
using Ocelot.Configuration.Builder;
using Ocelot.Middleware;
using Ocelot.Responses;
using Shouldly;
using System.Net.Http;
using Ocelot.Configuration;
using TestStack.BDDfy;
using Xunit;
@ -65,6 +66,20 @@ namespace Ocelot.UnitTests.Request
.Then(_ => ThenTheContexRequestIsMappedToADownstreamRequest())
.And(_ => ThenTheDownstreamRequestIsStored())
.And(_ => ThenTheNextMiddlewareIsInvoked())
.And(_ => ThenTheDownstreamRequestMethodIs("GET"))
.BDDfy();
}
[Fact]
public void Should_map_downstream_reroute_method_to_downstream_request()
{
this.Given(_ => GivenTheHttpContextContainsARequest())
.And(_ => GivenTheMapperWillReturnAMappedRequest())
.When(_ => WhenTheMiddlewareIsInvoked())
.Then(_ => ThenTheContexRequestIsMappedToADownstreamRequest())
.And(_ => ThenTheDownstreamRequestIsStored())
.And(_ => ThenTheNextMiddlewareIsInvoked())
.And(_ => ThenTheDownstreamRequestMethodIs("GET"))
.BDDfy();
}
@ -80,6 +95,11 @@ namespace Ocelot.UnitTests.Request
.BDDfy();
}
private void ThenTheDownstreamRequestMethodIs(string expected)
{
_downstreamContext.DownstreamRequest.Method.ShouldBe(expected);
}
private void GivenTheHttpContextContainsARequest()
{
_httpContext
@ -92,7 +112,7 @@ namespace Ocelot.UnitTests.Request
_mappedRequest = new OkResponse<HttpRequestMessage>(new HttpRequestMessage(HttpMethod.Get, "http://www.bbc.co.uk"));
_requestMapper
.Setup(rm => rm.Map(It.IsAny<HttpRequest>()))
.Setup(rm => rm.Map(It.IsAny<HttpRequest>(), It.IsAny<DownstreamReRoute>()))
.ReturnsAsync(_mappedRequest);
}
@ -101,7 +121,7 @@ namespace Ocelot.UnitTests.Request
_mappedRequest = new ErrorResponse<HttpRequestMessage>(new UnmappableRequestError(new System.Exception("boooom!")));
_requestMapper
.Setup(rm => rm.Map(It.IsAny<HttpRequest>()))
.Setup(rm => rm.Map(It.IsAny<HttpRequest>(), It.IsAny<DownstreamReRoute>()))
.ReturnsAsync(_mappedRequest);
}
@ -112,7 +132,7 @@ namespace Ocelot.UnitTests.Request
private void ThenTheContexRequestIsMappedToADownstreamRequest()
{
_requestMapper.Verify(rm => rm.Map(_httpRequest.Object), Times.Once);
_requestMapper.Verify(rm => rm.Map(_httpRequest.Object, _downstreamContext.DownstreamReRoute), Times.Once);
}
private void ThenTheDownstreamRequestIsStored()

View File

@ -13,6 +13,8 @@
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Ocelot.Configuration;
using Ocelot.Configuration.Builder;
using TestStack.BDDfy;
using Xunit;
@ -27,6 +29,8 @@
private List<KeyValuePair<string, StringValues>> _inputHeaders = null;
private DownstreamReRoute _downstreamReRoute;
public RequestMapperTests()
{
_httpContext = new DefaultHttpContext();
@ -82,6 +86,21 @@
.BDDfy();
}
[Theory]
[InlineData("", "GET")]
[InlineData(null, "GET")]
[InlineData("POST", "POST")]
public void Should_use_downstream_reroute_method_if_set(string input, string expected)
{
this.Given(_ => GivenTheInputRequestHasMethod("GET"))
.And(_ => GivenTheDownstreamReRouteMethodIs(input))
.And(_ => GivenTheInputRequestHasAValidUri())
.When(_ => WhenMapped())
.Then(_ => ThenNoErrorIsReturned())
.And(_ => ThenTheMappedRequestHasMethod(expected))
.BDDfy();
}
[Fact]
public void Should_map_all_headers()
{
@ -154,16 +173,6 @@
.BDDfy();
}
private void GivenTheInputRequestHasNoContentLength()
{
_inputRequest.ContentLength = null;
}
private void GivenTheInputRequestHasNoContentType()
{
_inputRequest.ContentType = null;
}
[Fact]
public void Should_map_content_headers()
{
@ -212,6 +221,22 @@
.BDDfy();
}
private void GivenTheDownstreamReRouteMethodIs(string input)
{
_downstreamReRoute = new DownstreamReRouteBuilder().WithDownStreamHttpMethod(input).Build();
}
private void GivenTheInputRequestHasNoContentLength()
{
_inputRequest.ContentLength = null;
}
private void GivenTheInputRequestHasNoContentType()
{
_inputRequest.ContentType = null;
}
private void ThenTheContentHeadersAreNotAddedToNonContentHeaders()
{
_mappedRequest.Data.Headers.ShouldNotContain(x => x.Key == "Content-Disposition");
@ -380,7 +405,7 @@
private async Task WhenMapped()
{
_mappedRequest = await _requestMapper.Map(_inputRequest);
_mappedRequest = await _requestMapper.Map(_inputRequest, _downstreamReRoute);
}
private void ThenNoErrorIsReturned()