mirror of
				https://github.com/nsnail/Ocelot.git
				synced 2025-10-31 10:19:26 +08:00 
			
		
		
		
	messing around with the proxy mdh the proxy middleware
This commit is contained in:
		| @@ -2,13 +2,13 @@ namespace Ocelot.Library.Infrastructure.HostUrlRepository | ||||
| { | ||||
|     public class HostUrlMap | ||||
|     { | ||||
|         public HostUrlMap(string downstreamHostUrl, string upstreamHostUrl) | ||||
|         public HostUrlMap(string urlPathTemplate, string upstreamHostUrl) | ||||
|         { | ||||
|             DownstreamHostUrl = downstreamHostUrl; | ||||
|             UrlPathTemplate = urlPathTemplate; | ||||
|             UpstreamHostUrl = upstreamHostUrl; | ||||
|         } | ||||
|  | ||||
|         public string DownstreamHostUrl {get;private set;} | ||||
|         public string UrlPathTemplate {get;private set;} | ||||
|         public string UpstreamHostUrl {get;private set;} | ||||
|     } | ||||
| } | ||||
| @@ -5,6 +5,6 @@ namespace Ocelot.Library.Infrastructure.HostUrlRepository | ||||
|     public interface IHostUrlMapRepository | ||||
|     { | ||||
|         Response AddBaseUrlMap(HostUrlMap baseUrlMap); | ||||
|         Response<HostUrlMap> GetBaseUrlMap(string downstreamUrl); | ||||
|         Response<HostUrlMap> GetBaseUrlMap(string urlPathTemplate); | ||||
|     } | ||||
| }  | ||||
| @@ -12,23 +12,23 @@ namespace Ocelot.Library.Infrastructure.HostUrlRepository | ||||
|         } | ||||
|         public Response AddBaseUrlMap(HostUrlMap baseUrlMap) | ||||
|         { | ||||
|             if(_routes.ContainsKey(baseUrlMap.DownstreamHostUrl))  | ||||
|             if(_routes.ContainsKey(baseUrlMap.UrlPathTemplate))  | ||||
|             { | ||||
|                 return new ErrorResponse(new List<Error>(){new HostUrlMapKeyAlreadyExists()}); | ||||
|             } | ||||
| 
 | ||||
|             _routes.Add(baseUrlMap.DownstreamHostUrl, baseUrlMap.UpstreamHostUrl); | ||||
|             _routes.Add(baseUrlMap.UrlPathTemplate, baseUrlMap.UpstreamHostUrl); | ||||
| 
 | ||||
|             return new OkResponse(); | ||||
|         } | ||||
| 
 | ||||
|         public Response<HostUrlMap> GetBaseUrlMap(string downstreamUrl) | ||||
|         public Response<HostUrlMap> GetBaseUrlMap(string urlPathTemplate) | ||||
|         { | ||||
|             string upstreamUrl = null; | ||||
| 
 | ||||
|             if(_routes.TryGetValue(downstreamUrl, out upstreamUrl)) | ||||
|             if(_routes.TryGetValue(urlPathTemplate, out upstreamUrl)) | ||||
|             { | ||||
|                 return new OkResponse<HostUrlMap>(new HostUrlMap(downstreamUrl, upstreamUrl)); | ||||
|                 return new OkResponse<HostUrlMap>(new HostUrlMap(urlPathTemplate, upstreamUrl)); | ||||
|             } | ||||
|      | ||||
|             return new ErrorResponse<HostUrlMap>(new List<Error>(){new HostUrlMapKeyDoesNotExist()}); | ||||
| @@ -4,12 +4,15 @@ namespace Ocelot.Library.Infrastructure.UrlPathMatcher | ||||
| { | ||||
|     public class UrlPathMatch | ||||
|     { | ||||
|         public UrlPathMatch(bool match, List<TemplateVariableNameAndValue> templateVariableNameAndValues) | ||||
|         public UrlPathMatch(bool match, List<TemplateVariableNameAndValue> templateVariableNameAndValues, string urlPathTemplate) | ||||
|         { | ||||
|             Match = match;  | ||||
|             TemplateVariableNameAndValues = templateVariableNameAndValues; | ||||
|             UrlPathTemplate = urlPathTemplate; | ||||
|         } | ||||
|         public bool Match {get;private set;} | ||||
|         public List<TemplateVariableNameAndValue> TemplateVariableNameAndValues {get;private set;} | ||||
|  | ||||
|         public string UrlPathTemplate {get;private set;} | ||||
|     } | ||||
| } | ||||
| @@ -7,6 +7,8 @@ namespace Ocelot.Library.Infrastructure.UrlPathMatcher | ||||
|     { | ||||
|         public UrlPathMatch Match(string urlPath, string urlPathTemplate) | ||||
|         { | ||||
|             var urlPathTemplateCopy = urlPathTemplate; | ||||
|  | ||||
|             var templateKeysAndValues = new List<TemplateVariableNameAndValue>(); | ||||
|  | ||||
|             urlPath = urlPath.ToLower(); | ||||
| @@ -37,12 +39,12 @@ namespace Ocelot.Library.Infrastructure.UrlPathMatcher | ||||
|                     }  | ||||
|                     else | ||||
|                     { | ||||
|                         return new UrlPathMatch(false, templateKeysAndValues); | ||||
|                         return new UrlPathMatch(false, templateKeysAndValues, string.Empty); | ||||
|                     }  | ||||
|                 } | ||||
|                 counterForUrl++; | ||||
|             } | ||||
|             return new UrlPathMatch(true, templateKeysAndValues); | ||||
|             return new UrlPathMatch(true, templateKeysAndValues, urlPathTemplateCopy); | ||||
|         } | ||||
|  | ||||
|         private string GetPlaceholderVariableValue(string urlPath, int counterForUrl) | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| using System.Collections.Generic; | ||||
| using Ocelot.Library.Infrastructure.Responses; | ||||
|  | ||||
| namespace Ocelot.Library.Infrastructure.UrlPathTemplateRepository | ||||
| @@ -6,5 +7,6 @@ namespace Ocelot.Library.Infrastructure.UrlPathTemplateRepository | ||||
|     { | ||||
|         Response AddUrlPathTemplateMap(UrlPathTemplateMap urlPathMap); | ||||
|         Response<UrlPathTemplateMap> GetUrlPathTemplateMap(string downstreamUrlPathTemplate); | ||||
|         Response<List<UrlPathTemplateMap>> All { get; } | ||||
|     } | ||||
| }    | ||||
| @@ -1,5 +1,6 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using Ocelot.Library.Infrastructure.Responses; | ||||
|  | ||||
| namespace Ocelot.Library.Infrastructure.UrlPathTemplateRepository | ||||
| @@ -11,6 +12,18 @@ namespace Ocelot.Library.Infrastructure.UrlPathTemplateRepository | ||||
|         { | ||||
|             _routes = new Dictionary<string,string>(); | ||||
|         } | ||||
|  | ||||
|         public Response<List<UrlPathTemplateMap>> All | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 var routes =  _routes | ||||
|                 .Select(r => new UrlPathTemplateMap(r.Key, r.Value)) | ||||
|                 .ToList(); | ||||
|                 return new OkResponse<List<UrlPathTemplateMap>>(routes); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public Response AddUrlPathTemplateMap(UrlPathTemplateMap urlPathMap) | ||||
|         { | ||||
|             if(_routes.ContainsKey(urlPathMap.DownstreamUrlPathTemplate)) | ||||
|   | ||||
| @@ -1,26 +1,61 @@ | ||||
| using System; | ||||
| using System.Threading.Tasks; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Ocelot.Library.Infrastructure.HostUrlRepository; | ||||
| using Ocelot.Library.Infrastructure.UrlPathMatcher; | ||||
| using Ocelot.Library.Infrastructure.UrlPathTemplateRepository; | ||||
|  | ||||
| namespace Ocelot.Library.Middleware | ||||
| { | ||||
|     public class ProxyMiddleware | ||||
|     { | ||||
|         private readonly RequestDelegate _next; | ||||
|  | ||||
|         public ProxyMiddleware(RequestDelegate next) | ||||
|         private readonly IUrlPathToUrlPathTemplateMatcher _urlMatcher; | ||||
|         private readonly IUrlPathTemplateMapRepository _urlPathRepository; | ||||
|         private readonly IHostUrlMapRepository _hostUrlRepository; | ||||
|         public ProxyMiddleware(RequestDelegate next,  | ||||
|             IUrlPathToUrlPathTemplateMatcher urlMatcher, | ||||
|             IUrlPathTemplateMapRepository urlPathRepository, | ||||
|             IHostUrlMapRepository hostUrlRepository) | ||||
|         { | ||||
|             _next = next; | ||||
|             _urlMatcher = urlMatcher; | ||||
|             _urlPathRepository = urlPathRepository; | ||||
|             _hostUrlRepository = hostUrlRepository; | ||||
|         } | ||||
|  | ||||
|         public async Task Invoke(HttpContext context) | ||||
|         { | ||||
|             //get the downstream host from the request context | ||||
|             //get the upstream host from the host repository | ||||
|             //if no upstream host fail this request | ||||
|             //get the downstream path from the request context | ||||
|             //get the downstream path template from the path template finder | ||||
|             //todo think about variables.. | ||||
|             //add any query string.. | ||||
|              | ||||
|             var path = context.Request.Path.ToString(); | ||||
|  | ||||
|             var templates = _urlPathRepository.All; | ||||
|  | ||||
|             UrlPathMatch urlPathMatch = null; | ||||
|             string upstreamPathUrl = string.Empty; | ||||
|  | ||||
|             foreach (var template in templates.Data) | ||||
|             { | ||||
|                 urlPathMatch = _urlMatcher.Match(path, template.DownstreamUrlPathTemplate); | ||||
|  | ||||
|                 if (urlPathMatch.Match) | ||||
|                 { | ||||
|                     upstreamPathUrl = template.UpstreamUrlPathTemplate; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (!urlPathMatch.Match) | ||||
|             { | ||||
|                 throw new Exception("BOOOM TING! no match"); | ||||
|             } | ||||
|              | ||||
|             var upstreamHostUrl = _hostUrlRepository.GetBaseUrlMap(urlPathMatch.UrlPathTemplate); | ||||
|  | ||||
|             //now map the variables from the url path to the upstream url path | ||||
|              | ||||
|  | ||||
|  | ||||
|             await _next.Invoke(context); | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -37,11 +37,11 @@ namespace Ocelot | ||||
|             loggerFactory.AddDebug(); | ||||
|  | ||||
|             app.UseProxy(); | ||||
|  | ||||
|             app.Run(async context => | ||||
|             { | ||||
|                 await context.Response.WriteAsync("Hello from Tom"); | ||||
|             }); | ||||
|             //app.Run() | ||||
|             // app.Run(async context => | ||||
|             // { | ||||
|             //     await context.Response.WriteAsync("Hello from Tom"); | ||||
|             // }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										34
									
								
								test/Ocelot.AcceptanceTests/Fake/FakeService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								test/Ocelot.AcceptanceTests/Fake/FakeService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| using System.IO; | ||||
| using System.Threading.Tasks; | ||||
| using Microsoft.AspNetCore.Hosting; | ||||
|  | ||||
| namespace Ocelot.AcceptanceTests.Fake | ||||
| { | ||||
|     public class FakeService | ||||
|     { | ||||
|         private Task _handler; | ||||
|         private IWebHost _webHostBuilder; | ||||
|  | ||||
|         public void Start(string url) | ||||
|         { | ||||
|             _webHostBuilder = new WebHostBuilder() | ||||
|                 .UseKestrel() | ||||
|                 .UseContentRoot(Directory.GetCurrentDirectory()) | ||||
|                 .UseIISIntegration() | ||||
|                 .UseUrls(url) | ||||
|                 .UseStartup<FakeStartup>() | ||||
|                 .Build(); | ||||
|  | ||||
|             _handler = Task.Run(() => _webHostBuilder.Run()); | ||||
|         } | ||||
|  | ||||
|         public void Stop() | ||||
|         { | ||||
|             if(_webHostBuilder != null) | ||||
|             { | ||||
|                 _webHostBuilder.Dispose(); | ||||
|                 _handler.Wait(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										40
									
								
								test/Ocelot.AcceptanceTests/Fake/FakeStartup.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								test/Ocelot.AcceptanceTests/Fake/FakeStartup.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| using Microsoft.AspNetCore.Builder; | ||||
| using Microsoft.AspNetCore.Hosting; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.Extensions.Configuration; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| using Microsoft.Extensions.Logging; | ||||
|  | ||||
| namespace Ocelot.AcceptanceTests.Fake | ||||
| { | ||||
|     public class FakeStartup | ||||
|     { | ||||
|         public FakeStartup(IHostingEnvironment env) | ||||
|         { | ||||
|             var builder = new ConfigurationBuilder() | ||||
|                 .SetBasePath(env.ContentRootPath) | ||||
|                 .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) | ||||
|                 .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) | ||||
|                 .AddEnvironmentVariables(); | ||||
|             Configuration = builder.Build(); | ||||
|         } | ||||
|  | ||||
|         public IConfigurationRoot Configuration { get; } | ||||
|  | ||||
|         // This method gets called by the runtime. Use this method to add services to the container. | ||||
|         public void ConfigureServices(IServiceCollection services) | ||||
|         { | ||||
|             // Add framework services. | ||||
|              | ||||
|         } | ||||
|  | ||||
|         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. | ||||
|         public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) | ||||
|         { | ||||
|             app.Run(async context => | ||||
|             { | ||||
|                 await context.Response.WriteAsync("Hello from Laura"); | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -4,38 +4,51 @@ using System.Threading.Tasks; | ||||
| using Microsoft.AspNetCore.Hosting; | ||||
| using Microsoft.AspNetCore.TestHost; | ||||
| using Xunit; | ||||
| using Ocelot.AcceptanceTests.Fake; | ||||
| using Shouldly; | ||||
|  | ||||
| namespace Ocelot.AcceptanceTests | ||||
| { | ||||
|     public class RouterTests : IDisposable | ||||
|     { | ||||
|         private FakeService _fakeService; | ||||
|         private readonly TestServer _server; | ||||
|         private readonly HttpClient _client; | ||||
|  | ||||
|         public RouterTests() | ||||
|         { | ||||
|             // Arrange | ||||
|             _server = new TestServer(new WebHostBuilder() | ||||
|                 .UseStartup<Startup>()); | ||||
|             _client = _server.CreateClient(); | ||||
|             _fakeService = new FakeService(); | ||||
|         } | ||||
|  | ||||
|         [Fact] | ||||
|         public async Task ReturnHelloWorld() | ||||
|         public void hello_world() | ||||
|         { | ||||
|             var response = _client.GetAsync("/").Result; | ||||
|             response.EnsureSuccessStatusCode(); | ||||
|  | ||||
|             var responseString = response.Content.ReadAsStringAsync().Result; | ||||
|             responseString.ShouldBe("Hello from Tom");       | ||||
|         } | ||||
|  | ||||
|         [Fact] | ||||
|         public async Task can_route_request() | ||||
|         {             | ||||
|             _fakeService.Start("http://localhost:5001"); | ||||
|  | ||||
|             // Act | ||||
|             var response = await _client.GetAsync("/"); | ||||
|             response.EnsureSuccessStatusCode(); | ||||
|  | ||||
|             var responseString = await response.Content.ReadAsStringAsync(); | ||||
|  | ||||
|             // Assert | ||||
|             Assert.Equal("Hello from Tom", | ||||
|                 responseString); | ||||
|             responseString.ShouldBe("Hello from Laura"); | ||||
|         } | ||||
|  | ||||
|         public void Dispose() | ||||
|         { | ||||
|         {                | ||||
|             _fakeService.Stop(); | ||||
|             _client.Dispose(); | ||||
|             _server.Dispose(); | ||||
|         } | ||||
|   | ||||
| @@ -77,7 +77,7 @@ namespace Ocelot.UnitTests | ||||
|  | ||||
|         private void ThenTheRouteIsReturned() | ||||
|         { | ||||
|             _getRouteResponse.Data.DownstreamHostUrl.ShouldBe(_downstreamBaseUrl); | ||||
|             _getRouteResponse.Data.UrlPathTemplate.ShouldBe(_downstreamBaseUrl); | ||||
|             _getRouteResponse.Data.UpstreamHostUrl.ShouldBe(_upstreamBaseUrl); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| using System.Collections.Generic; | ||||
| using Ocelot.Library.Infrastructure.Responses; | ||||
| using Ocelot.Library.Infrastructure.UrlPathTemplateRepository; | ||||
| using Shouldly; | ||||
| @@ -12,6 +13,7 @@ namespace Ocelot.UnitTests | ||||
|         private IUrlPathTemplateMapRepository _repository; | ||||
|         private Response _response; | ||||
|         private Response<UrlPathTemplateMap> _getResponse; | ||||
|         private Response<List<UrlPathTemplateMap>> _listResponse; | ||||
|  | ||||
|         public UrlPathTemplateMapRepositoryTests()  | ||||
|         { | ||||
| @@ -34,6 +36,14 @@ namespace Ocelot.UnitTests | ||||
|             WhenIRetrieveTheUrlPathByDownstreamUrl(); | ||||
|             ThenTheUrlPathIsReturned(); | ||||
|         } | ||||
|  | ||||
|         [Fact] | ||||
|         public void can_get_all_urls() | ||||
|         { | ||||
|             GivenIHaveSetUpADownstreamUrlPathAndAnUpstreamUrlPath("/api2", "http://www.someapi.com/api2"); | ||||
|             WhenIRetrieveTheUrls(); | ||||
|             ThenTheUrlsAreReturned(); | ||||
|         } | ||||
|   | ||||
|         [Fact] | ||||
|         public void should_return_error_response_when_url_path_already_used() | ||||
| @@ -75,12 +85,22 @@ namespace Ocelot.UnitTests | ||||
|             _getResponse = _repository.GetUrlPathTemplateMap(_downstreamUrlPath); | ||||
|         } | ||||
|  | ||||
|            private void WhenIRetrieveTheUrls() | ||||
|         { | ||||
|             _listResponse = _repository.All; | ||||
|         } | ||||
|  | ||||
|         private void ThenTheUrlPathIsReturned() | ||||
|         { | ||||
|             _getResponse.Data.DownstreamUrlPathTemplate.ShouldBe(_downstreamUrlPath); | ||||
|             _getResponse.Data.UpstreamUrlPathTemplate.ShouldBe(_upstreamUrlPath); | ||||
|         } | ||||
|  | ||||
|         private void ThenTheUrlsAreReturned() | ||||
|         { | ||||
|             _listResponse.Data.Count.ShouldBeGreaterThan(0); | ||||
|         } | ||||
|  | ||||
|         private void GivenIHaveSetUpADownstreamUrlPathAndAnUpstreamUrlPath(string downstream, string upstreamApiUrl) | ||||
|         { | ||||
|             GivenIHaveAnUpstreamUrlPath(upstreamApiUrl); | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using Ocelot.Library.Infrastructure.UrlPathMatcher; | ||||
| @@ -25,7 +26,7 @@ namespace Ocelot.UnitTests | ||||
|             WhenIMatchThePaths(); | ||||
|             ThenTheResultIsTrue(); | ||||
|             ThenTheTemplatesDictionaryIs(new List<TemplateVariableNameAndValue>()); | ||||
|  | ||||
|             ThenTheUrlPathTemplateIs("api/product/products/"); | ||||
|         } | ||||
|  | ||||
|         [Fact] | ||||
| @@ -41,6 +42,7 @@ namespace Ocelot.UnitTests | ||||
|             WhenIMatchThePaths(); | ||||
|             ThenTheResultIsTrue(); | ||||
|             ThenTheTemplatesDictionaryIs(expectedTemplates); | ||||
|             ThenTheUrlPathTemplateIs("api/product/products/{productId}/variants/"); | ||||
|         } | ||||
|  | ||||
|         [Fact] | ||||
| @@ -56,6 +58,8 @@ namespace Ocelot.UnitTests | ||||
|             WhenIMatchThePaths(); | ||||
|             ThenTheResultIsTrue(); | ||||
|             ThenTheTemplatesDictionaryIs(expectedTemplates); | ||||
|             ThenTheUrlPathTemplateIs("api/product/products/{productId}"); | ||||
|  | ||||
|         } | ||||
|  | ||||
|         [Fact] | ||||
| @@ -72,6 +76,8 @@ namespace Ocelot.UnitTests | ||||
|             WhenIMatchThePaths(); | ||||
|             ThenTheResultIsTrue(); | ||||
|             ThenTheTemplatesDictionaryIs(expectedTemplates); | ||||
|             ThenTheUrlPathTemplateIs("api/product/products/{productId}/{categoryId}"); | ||||
|  | ||||
|         } | ||||
|  | ||||
|         [Fact] | ||||
| @@ -88,6 +94,8 @@ namespace Ocelot.UnitTests | ||||
|             WhenIMatchThePaths(); | ||||
|             ThenTheResultIsTrue(); | ||||
|             ThenTheTemplatesDictionaryIs(expectedTemplates); | ||||
|             ThenTheUrlPathTemplateIs("api/product/products/{productId}/categories/{categoryId}"); | ||||
|  | ||||
|         } | ||||
|  | ||||
|         [Fact] | ||||
| @@ -105,6 +113,8 @@ namespace Ocelot.UnitTests | ||||
|             WhenIMatchThePaths(); | ||||
|             ThenTheResultIsTrue(); | ||||
|             ThenTheTemplatesDictionaryIs(expectedTemplates); | ||||
|             ThenTheUrlPathTemplateIs("api/product/products/{productId}/categories/{categoryId}/variant/{variantId}"); | ||||
|  | ||||
|         } | ||||
|  | ||||
|         [Fact] | ||||
| @@ -121,6 +131,8 @@ namespace Ocelot.UnitTests | ||||
|             WhenIMatchThePaths(); | ||||
|             ThenTheResultIsTrue(); | ||||
|             ThenTheTemplatesDictionaryIs(expectedTemplates); | ||||
|             ThenTheUrlPathTemplateIs("api/product/products/{productId}/categories/{categoryId}/variant/"); | ||||
|  | ||||
|         } | ||||
|  | ||||
|         private void ThenTheTemplatesDictionaryIs(List<TemplateVariableNameAndValue> expectedResults) | ||||
| @@ -133,6 +145,10 @@ namespace Ocelot.UnitTests | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void ThenTheUrlPathTemplateIs(string expectedUrlPathTemplate) | ||||
|         { | ||||
|             _result.UrlPathTemplate.ShouldBe(expectedUrlPathTemplate); | ||||
|         } | ||||
|         private void GivenIHaveADownstreamPath(string downstreamPath) | ||||
|         { | ||||
|             _downstreamPath = downstreamPath; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Tom Gardham-Pallister
					Tom Gardham-Pallister