split DownstreamTemplate into DownstreamPathTemplate, DownstreamScheme, DownstreamHost and DownstreamPort in order to prepare for service discovery

This commit is contained in:
TomPallister
2017-01-21 09:59:47 +00:00
parent 044b609ea9
commit 0f71c040d9
53 changed files with 767 additions and 258 deletions

View File

@ -1,11 +1,12 @@
using System;
using System.Collections.Generic;
using Ocelot.Values;
namespace Ocelot.Configuration.Builder
{
public class ReRouteBuilder
{
private string _downstreamTemplate;
private string _downstreamPathTemplate;
private string _upstreamTemplate;
private string _upstreamTemplatePattern;
private string _upstreamHttpMethod;
@ -30,6 +31,7 @@ namespace Ocelot.Configuration.Builder
private string _serviceDiscoveryAddress;
private string _downstreamScheme;
private string _downstreamHost;
private int _dsPort;
public ReRouteBuilder()
{
@ -72,9 +74,9 @@ namespace Ocelot.Configuration.Builder
return this;
}
public ReRouteBuilder WithDownstreamTemplate(string input)
public ReRouteBuilder WithDownstreamPathTemplate(string input)
{
_downstreamTemplate = input;
_downstreamPathTemplate = input;
return this;
}
@ -184,11 +186,17 @@ namespace Ocelot.Configuration.Builder
return this;
}
public ReRouteBuilder WithDownstreamPort(int port)
{
_dsPort = port;
return this;
}
public ReRoute Build()
{
Func<string> downstreamHostFunc = () => { return _downstreamHost; };
Func<HostAndPort> downstreamHostFunc = () => new HostAndPort(_downstreamHost, _dsPort);
return new ReRoute(_downstreamTemplate, _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern,
return new ReRoute(new DownstreamPathTemplate(_downstreamPathTemplate), _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern,
_isAuthenticated, new AuthenticationOptions(_authenticationProvider, _authenticationProviderUrl, _scopeName,
_requireHttps, _additionalScopes, _scopeSecret), _configHeaderExtractorProperties, _claimToClaims, _routeClaimRequirement,
_isAuthorised, _claimToQueries, _requestIdHeaderKey, _isCached, _fileCacheOptions, _serviceName,

View File

@ -8,6 +8,7 @@ using Ocelot.Configuration.Parser;
using Ocelot.Configuration.Validator;
using Ocelot.Responses;
using Ocelot.Utilities;
using Ocelot.Values;
namespace Ocelot.Configuration.Creator
{
@ -96,7 +97,7 @@ namespace Ocelot.Configuration.Creator
&& !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Provider);
Func<string> downstreamHostFunc = () => { return reRoute.DownstreamHost; };
Func<HostAndPort> downstreamHostAndPortFunc = () => new HostAndPort(reRoute.DownstreamHost.Trim('/'), reRoute.DownstreamPort);
if (isAuthenticated)
{
@ -109,22 +110,22 @@ namespace Ocelot.Configuration.Creator
var claimsToClaims = GetAddThingsToRequest(reRoute.AddClaimsToRequest);
var claimsToQueries = GetAddThingsToRequest(reRoute.AddQueriesToRequest);
return new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate,
return new ReRoute(new DownstreamPathTemplate(reRoute.DownstreamPathTemplate), reRoute.UpstreamTemplate,
reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated,
authOptionsForRoute, claimsToHeaders, claimsToClaims,
reRoute.RouteClaimsRequirement, isAuthorised, claimsToQueries,
requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds),
reRoute.ServiceName, useServiceDiscovery, globalConfiguration?.ServiceDiscoveryProvider?.Provider,
globalConfiguration?.ServiceDiscoveryProvider?.Address, downstreamHostFunc, reRoute.DownstreamScheme);
globalConfiguration?.ServiceDiscoveryProvider?.Address, downstreamHostAndPortFunc, reRoute.DownstreamScheme);
}
return new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate,
return new ReRoute(new DownstreamPathTemplate(reRoute.DownstreamPathTemplate), reRoute.UpstreamTemplate,
reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated,
null, new List<ClaimToThing>(), new List<ClaimToThing>(),
reRoute.RouteClaimsRequirement, isAuthorised, new List<ClaimToThing>(),
requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds),
reRoute.ServiceName, useServiceDiscovery, globalConfiguration?.ServiceDiscoveryProvider?.Provider,
globalConfiguration?.ServiceDiscoveryProvider?.Address, downstreamHostFunc, reRoute.DownstreamScheme);
globalConfiguration?.ServiceDiscoveryProvider?.Address, downstreamHostAndPortFunc, reRoute.DownstreamScheme);
}
private string BuildUpstreamTemplate(FileReRoute reRoute)

View File

@ -14,7 +14,7 @@ namespace Ocelot.Configuration.File
FileCacheOptions = new FileCacheOptions();
}
public string DownstreamTemplate { get; set; }
public string DownstreamPathTemplate { get; set; }
public string UpstreamTemplate { get; set; }
public string UpstreamHttpMethod { get; set; }
public FileAuthenticationOptions AuthenticationOptions { get; set; }
@ -28,5 +28,6 @@ namespace Ocelot.Configuration.File
public string ServiceName { get; set; }
public string DownstreamScheme {get;set;}
public string DownstreamHost {get;set;}
public int DownstreamPort { get; set; }
}
}

View File

@ -1,17 +1,18 @@
using System;
using System.Collections.Generic;
using Ocelot.Values;
namespace Ocelot.Configuration
{
public class ReRoute
{
public ReRoute(string downstreamTemplate, string upstreamTemplate, string upstreamHttpMethod, string upstreamTemplatePattern,
public ReRoute(DownstreamPathTemplate downstreamPathTemplate, string upstreamTemplate, string upstreamHttpMethod, string upstreamTemplatePattern,
bool isAuthenticated, AuthenticationOptions authenticationOptions, List<ClaimToThing> configurationHeaderExtractorProperties,
List<ClaimToThing> claimsToClaims, Dictionary<string, string> routeClaimsRequirement, bool isAuthorised, List<ClaimToThing> claimsToQueries,
string requestIdKey, bool isCached, CacheOptions fileCacheOptions, string serviceName, bool useServiceDiscovery,
string serviceDiscoveryProvider, string serviceDiscoveryAddress, Func<string> downstreamHost, string downstreamScheme)
string serviceDiscoveryProvider, string serviceDiscoveryAddress, Func<HostAndPort> downstreamHostAndPort, string downstreamScheme)
{
DownstreamTemplate = downstreamTemplate;
DownstreamPathTemplate = downstreamPathTemplate;
UpstreamTemplate = upstreamTemplate;
UpstreamHttpMethod = upstreamHttpMethod;
UpstreamTemplatePattern = upstreamTemplatePattern;
@ -32,11 +33,11 @@ namespace Ocelot.Configuration
UseServiceDiscovery = useServiceDiscovery;
ServiceDiscoveryProvider = serviceDiscoveryProvider;
ServiceDiscoveryAddress = serviceDiscoveryAddress;
DownstreamHost = downstreamHost;
DownstreamHostAndPort = downstreamHostAndPort;
DownstreamScheme = downstreamScheme;
}
public string DownstreamTemplate { get; private set; }
public DownstreamPathTemplate DownstreamPathTemplate { get; private set; }
public string UpstreamTemplate { get; private set; }
public string UpstreamTemplatePattern { get; private set; }
public string UpstreamHttpMethod { get; private set; }
@ -54,7 +55,7 @@ namespace Ocelot.Configuration
public bool UseServiceDiscovery { get; private set;}
public string ServiceDiscoveryProvider { get; private set;}
public string ServiceDiscoveryAddress { get; private set;}
public Func<string> DownstreamHost {get;private set;}
public Func<HostAndPort> DownstreamHostAndPort {get;private set;}
public string DownstreamScheme {get;private set;}
}
}

View File

@ -0,0 +1,11 @@
using Ocelot.Errors;
namespace Ocelot.Configuration.Validator
{
public class DownstreamPathTemplateAlreadyUsedError : Error
{
public DownstreamPathTemplateAlreadyUsedError(string message) : base(message, OcelotErrorCode.DownstreampathTemplateAlreadyUsedError)
{
}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Errors;
namespace Ocelot.Configuration.Validator
{
public class DownstreamPathTemplateContainsSchemeError : Error
{
public DownstreamPathTemplateContainsSchemeError(string message)
: base(message, OcelotErrorCode.DownstreamPathTemplateContainsSchemeError)
{
}
}
}

View File

@ -1,11 +0,0 @@
using Ocelot.Errors;
namespace Ocelot.Configuration.Validator
{
public class DownstreamTemplateAlreadyUsedError : Error
{
public DownstreamTemplateAlreadyUsedError(string message) : base(message, OcelotErrorCode.DownstreamTemplateAlreadyUsedError)
{
}
}
}

View File

@ -1,12 +0,0 @@
using Ocelot.Errors;
namespace Ocelot.Configuration.Validator
{
public class DownstreamTemplateContainsHostError : Error
{
public DownstreamTemplateContainsHostError(string message)
: base(message, OcelotErrorCode.DownstreamTemplateContainsHostError)
{
}
}
}

View File

@ -1,12 +0,0 @@
using Ocelot.Errors;
namespace Ocelot.Configuration.Validator
{
public class DownstreamTemplateContainsSchemeError : Error
{
public DownstreamTemplateContainsSchemeError(string message)
: base(message, OcelotErrorCode.DownstreamTemplateContainsSchemeError)
{
}
}
}

View File

@ -26,7 +26,7 @@ namespace Ocelot.Configuration.Validator
return new OkResponse<ConfigurationValidationResult>(result);
}
result = CheckForReRoutesContainingDownstreamScheme(configuration);
result = CheckForReRoutesContainingDownstreamSchemeInDownstreamPathTemplate(configuration);
if (result.IsError)
{
@ -70,25 +70,25 @@ namespace Ocelot.Configuration.Validator
return Enum.TryParse(provider, true, out supportedProvider);
}
private ConfigurationValidationResult CheckForReRoutesContainingDownstreamScheme(FileConfiguration configuration)
private ConfigurationValidationResult CheckForReRoutesContainingDownstreamSchemeInDownstreamPathTemplate(FileConfiguration configuration)
{
var errors = new List<Error>();
foreach(var reRoute in configuration.ReRoutes)
{
if(reRoute.DownstreamTemplate.Contains("https://")
|| reRoute.DownstreamTemplate.Contains("http://"))
if(reRoute.DownstreamPathTemplate.Contains("https://")
|| reRoute.DownstreamPathTemplate.Contains("http://"))
{
errors.Add(new DownstreamTemplateContainsSchemeError($"{reRoute.DownstreamTemplate} contains scheme"));
errors.Add(new DownstreamPathTemplateContainsSchemeError($"{reRoute.DownstreamPathTemplate} contains scheme"));
}
}
if(errors.Any())
{
return new ConfigurationValidationResult(false, errors);
return new ConfigurationValidationResult(true, errors);
}
return new ConfigurationValidationResult(true, errors);
return new ConfigurationValidationResult(false, errors);
}
private ConfigurationValidationResult CheckForDupliateReRoutes(FileConfiguration configuration)
@ -105,7 +105,7 @@ namespace Ocelot.Configuration.Validator
.Where(x => x.Skip(1).Any());
var errors = dupes
.Select(d => new DownstreamTemplateAlreadyUsedError(string.Format("Duplicate DownstreamTemplate: {0}", d.Key.UpstreamTemplate)))
.Select(d => new DownstreamPathTemplateAlreadyUsedError(string.Format("Duplicate DownstreamPath: {0}", d.Key.UpstreamTemplate)))
.Cast<Error>()
.ToList();

View File

@ -18,6 +18,7 @@ using Ocelot.Configuration.Repository;
using Ocelot.Configuration.Validator;
using Ocelot.DownstreamRouteFinder.Finder;
using Ocelot.DownstreamRouteFinder.UrlMatcher;
using Ocelot.DownstreamUrlCreator;
using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer;
using Ocelot.Headers;
using Ocelot.Infrastructure.Claims.Parser;
@ -59,6 +60,7 @@ namespace Ocelot.DependencyInjection
services.AddMvcCore().AddJsonFormatters();
services.AddLogging();
services.AddSingleton<IOcelotLoggerFactory, AspDotNetLoggerFactory>();
services.AddSingleton<IUrlBuilder, UrlBuilder>();
services.AddSingleton<IRemoveOutputHeaders, RemoveOutputHeaders>();
services.AddSingleton<IOcelotConfigurationProvider, OcelotConfigurationProvider>();
services.AddSingleton<IClaimToThingConfigurationParser, ClaimToThingConfigurationParser>();
@ -69,7 +71,7 @@ namespace Ocelot.DependencyInjection
services.AddSingleton<IClaimsParser, ClaimsParser>();
services.AddSingleton<IUrlPathToUrlTemplateMatcher, RegExUrlMatcher>();
services.AddSingleton<IUrlPathPlaceholderNameAndValueFinder, UrlPathPlaceholderNameAndValueFinder>();
services.AddSingleton<IDownstreamUrlPathPlaceholderReplacer, DownstreamUrlPathPlaceholderReplacer>();
services.AddSingleton<IDownstreamPathPlaceholderReplacer, DownstreamTemplatePathPlaceholderReplacer>();
services.AddSingleton<IDownstreamRouteFinder, DownstreamRouteFinder.Finder.DownstreamRouteFinder>();
services.AddSingleton<IHttpRequester, HttpClientHttpRequester>();
services.AddSingleton<IHttpResponder, HttpContextResponder>();

View File

@ -44,7 +44,7 @@ namespace Ocelot.DownstreamRouteFinder.Middleware
return;
}
_logger.LogDebug("downstream template is {downstreamRoute.Data.ReRoute.DownstreamTemplate}", downstreamRoute.Data.ReRoute.DownstreamTemplate);
_logger.LogDebug("downstream template is {downstreamRoute.Data.ReRoute.DownstreamPath}", downstreamRoute.Data.ReRoute.DownstreamPathTemplate);
SetDownstreamRouteForThisRequest(downstreamRoute.Data);

View File

@ -0,0 +1,12 @@
using Ocelot.Errors;
namespace Ocelot.DownstreamUrlCreator
{
public class DownstreamHostNullOrEmptyError : Error
{
public DownstreamHostNullOrEmptyError()
: base("downstream host was null or empty", OcelotErrorCode.DownstreamHostNullOrEmptyError)
{
}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Errors;
namespace Ocelot.DownstreamUrlCreator
{
public class DownstreamPathNullOrEmptyError : Error
{
public DownstreamPathNullOrEmptyError()
: base("downstream path was null or empty", OcelotErrorCode.DownstreamPathNullOrEmptyError)
{
}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Errors;
namespace Ocelot.DownstreamUrlCreator
{
public class DownstreamSchemeNullOrEmptyError : Error
{
public DownstreamSchemeNullOrEmptyError()
: base("downstream scheme was null or empty", OcelotErrorCode.DownstreamSchemeNullOrEmptyError)
{
}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Configuration;
using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer;
using Ocelot.Responses;
using Ocelot.Values;
namespace Ocelot.DownstreamUrlCreator
{
public interface IUrlBuilder
{
Response<DownstreamUrl> Build(string downstreamPath, string downstreamScheme, HostAndPort downstreamHostAndPort);
}
}

View File

@ -1,6 +1,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Ocelot.Configuration;
using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer;
using Ocelot.Infrastructure.RequestData;
using Ocelot.Logging;
@ -11,17 +12,20 @@ namespace Ocelot.DownstreamUrlCreator.Middleware
public class DownstreamUrlCreatorMiddleware : OcelotMiddleware
{
private readonly RequestDelegate _next;
private readonly IDownstreamUrlPathPlaceholderReplacer _urlReplacer;
private readonly IDownstreamPathPlaceholderReplacer _replacer;
private readonly IOcelotLogger _logger;
private readonly IUrlBuilder _urlBuilder;
public DownstreamUrlCreatorMiddleware(RequestDelegate next,
IOcelotLoggerFactory loggerFactory,
IDownstreamUrlPathPlaceholderReplacer urlReplacer,
IRequestScopedDataRepository requestScopedDataRepository)
IDownstreamPathPlaceholderReplacer replacer,
IRequestScopedDataRepository requestScopedDataRepository,
IUrlBuilder urlBuilder)
:base(requestScopedDataRepository)
{
_next = next;
_urlReplacer = urlReplacer;
_replacer = replacer;
_urlBuilder = urlBuilder;
_logger = loggerFactory.CreateLogger<DownstreamUrlCreatorMiddleware>();
}
@ -29,19 +33,34 @@ namespace Ocelot.DownstreamUrlCreator.Middleware
{
_logger.LogDebug("started calling downstream url creator middleware");
var downstreamUrl = _urlReplacer.Replace(DownstreamRoute.ReRoute.DownstreamTemplate, DownstreamRoute.TemplatePlaceholderNameAndValues);
var dsPath = _replacer
.Replace(DownstreamRoute.ReRoute.DownstreamPathTemplate, DownstreamRoute.TemplatePlaceholderNameAndValues);
if (downstreamUrl.IsError)
if (dsPath.IsError)
{
_logger.LogDebug("IDownstreamUrlPathPlaceholderReplacer returned an error, setting pipeline error");
_logger.LogDebug("IDownstreamPathPlaceholderReplacer returned an error, setting pipeline error");
SetPipelineError(downstreamUrl.Errors);
SetPipelineError(dsPath.Errors);
return;
}
_logger.LogDebug("downstream url is {downstreamUrl.Data.Value}", downstreamUrl.Data.Value);
var dsScheme = DownstreamRoute.ReRoute.DownstreamScheme;
SetDownstreamUrlForThisRequest(downstreamUrl.Data.Value);
var dsHostAndPort = DownstreamRoute.ReRoute.DownstreamHostAndPort();
var dsUrl = _urlBuilder.Build(dsPath.Data.Value, dsScheme, dsHostAndPort);
if (dsUrl.IsError)
{
_logger.LogDebug("IUrlBuilder returned an error, setting pipeline error");
SetPipelineError(dsUrl.Errors);
return;
}
_logger.LogDebug("downstream url is {downstreamUrl.Data.Value}", dsUrl.Data.Value);
SetDownstreamUrlForThisRequest(dsUrl.Data.Value);
_logger.LogDebug("calling next middleware");

View File

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using Ocelot.Configuration;
using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer;
using Ocelot.Errors;
using Ocelot.Responses;
using Ocelot.Values;
namespace Ocelot.DownstreamUrlCreator
{
public class UrlBuilder : IUrlBuilder
{
public Response<DownstreamUrl> Build(string downstreamPath, string downstreamScheme, HostAndPort downstreamHostAndPort)
{
if (string.IsNullOrEmpty(downstreamPath))
{
return new ErrorResponse<DownstreamUrl>(new List<Error> {new DownstreamPathNullOrEmptyError()});
}
if (string.IsNullOrEmpty(downstreamScheme))
{
return new ErrorResponse<DownstreamUrl>(new List<Error> { new DownstreamSchemeNullOrEmptyError() });
}
if (string.IsNullOrEmpty(downstreamHostAndPort.DownstreamHost))
{
return new ErrorResponse<DownstreamUrl>(new List<Error> { new DownstreamHostNullOrEmptyError() });
}
var builder = new UriBuilder
{
Host = downstreamHostAndPort.DownstreamHost,
Path = downstreamPath,
Scheme = downstreamScheme
};
if (downstreamHostAndPort.DownstreamPort > 0)
{
builder.Port = downstreamHostAndPort.DownstreamPort;
}
var url = builder.Uri.ToString();
return new OkResponse<DownstreamUrl>(new DownstreamUrl(url));
}
}
}

View File

@ -1,25 +1,25 @@
using System.Collections.Generic;
using System.Text;
using Ocelot.DownstreamRouteFinder;
using Ocelot.DownstreamRouteFinder.UrlMatcher;
using Ocelot.Responses;
using Ocelot.Values;
namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer
{
public class DownstreamUrlPathPlaceholderReplacer : IDownstreamUrlPathPlaceholderReplacer
public class DownstreamTemplatePathPlaceholderReplacer : IDownstreamPathPlaceholderReplacer
{
public Response<DownstreamUrl> Replace(string downstreamTemplate, List<UrlPathPlaceholderNameAndValue> urlPathPlaceholderNameAndValues)
public Response<DownstreamPath> Replace(DownstreamPathTemplate downstreamPathTemplate, List<UrlPathPlaceholderNameAndValue> urlPathPlaceholderNameAndValues)
{
var upstreamUrl = new StringBuilder();
var downstreamPath = new StringBuilder();
upstreamUrl.Append(downstreamTemplate);
downstreamPath.Append(downstreamPathTemplate.Value);
foreach (var placeholderVariableAndValue in urlPathPlaceholderNameAndValues)
{
upstreamUrl.Replace(placeholderVariableAndValue.TemplateVariableName, placeholderVariableAndValue.TemplateVariableValue);
downstreamPath.Replace(placeholderVariableAndValue.TemplateVariableName, placeholderVariableAndValue.TemplateVariableValue);
}
return new OkResponse<DownstreamUrl>(new DownstreamUrl(upstreamUrl.ToString()));
return new OkResponse<DownstreamPath>(new DownstreamPath(downstreamPath.ToString()));
}
}
}

View File

@ -1,11 +1,12 @@
using System.Collections.Generic;
using Ocelot.DownstreamRouteFinder.UrlMatcher;
using Ocelot.Responses;
using Ocelot.Values;
namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer
{
public interface IDownstreamUrlPathPlaceholderReplacer
public interface IDownstreamPathPlaceholderReplacer
{
Response<DownstreamUrl> Replace(string downstreamTemplate, List<UrlPathPlaceholderNameAndValue> urlPathPlaceholderNameAndValues);
Response<DownstreamPath> Replace(DownstreamPathTemplate downstreamPathTemplate, List<UrlPathPlaceholderNameAndValue> urlPathPlaceholderNameAndValues);
}
}

View File

@ -4,7 +4,7 @@
{
UnauthenticatedError,
UnknownError,
DownstreamTemplateAlreadyUsedError,
DownstreampathTemplateAlreadyUsedError,
UnableToFindDownstreamRouteError,
CannotAddDataError,
CannotFindDataError,
@ -18,7 +18,9 @@
UnauthorizedError,
ClaimValueNotAuthorisedError,
UserDoesNotHaveClaimError,
DownstreamTemplateContainsSchemeError,
DownstreamTemplateContainsHostError
DownstreamPathTemplateContainsSchemeError,
DownstreamPathNullOrEmptyError,
DownstreamSchemeNullOrEmptyError,
DownstreamHostNullOrEmptyError
}
}

View File

@ -0,0 +1,12 @@
namespace Ocelot.Values
{
public class DownstreamPath
{
public DownstreamPath(string value)
{
Value = value;
}
public string Value { get; private set; }
}
}

View File

@ -0,0 +1,12 @@
namespace Ocelot.Values
{
public class DownstreamPathTemplate
{
public DownstreamPathTemplate(string value)
{
Value = value;
}
public string Value { get; private set; }
}
}

View File

@ -1,4 +1,4 @@
namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer
namespace Ocelot.Values
{
public class DownstreamUrl
{
@ -9,4 +9,4 @@
public string Value { get; private set; }
}
}
}

View File

@ -0,0 +1,14 @@
namespace Ocelot.Values
{
public class HostAndPort
{
public HostAndPort(string downstreamHost, int downstreamPort)
{
DownstreamHost = downstreamHost;
DownstreamPort = downstreamPort;
}
public string DownstreamHost { get; private set; }
public int DownstreamPort { get; private set; }
}
}